{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}

-- | WARNING: This module should be considered private! If you find yourself
-- wanting to import something from this module, please open an issue to get
-- that thing exported from "Salve".
module Salve.Internal
  ( Version(..)
  , PreRelease(..)
  , Build(..)
  , Constraint(..)
  , makeVersion
  , initialVersion
  , parseVersion
  , parsePreRelease
  , parseBuild
  , parseConstraint
  , unsafeParseVersion
  , unsafeParsePreRelease
  , unsafeParseBuild
  , unsafeParseConstraint
  , renderVersion
  , renderPreRelease
  , renderBuild
  , renderConstraint
  , isUnstable
  , isStable
  , fromBaseVersion
  , toBaseVersion
  , bumpMajor
  , bumpMinor
  , bumpPatch
  , satisfiesConstraint
  , majorLens
  , minorLens
  , patchLens
  , preReleasesLens
  , buildsLens
  , Operator(..)
  , Wildcard(..)
  , constraintLT
  , constraintLE
  , constraintEQ
  , constraintGE
  , constraintGT
  , constraintAnd
  , constraintOr
  , constraintHyphen
  , constraintTilde
  , constraintCaret
  ) where

import qualified Control.Monad as Monad
import qualified Data.Char as Char
import qualified Data.Data as Data
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import qualified Data.Monoid as Monoid
import qualified Data.Ord as Ord
import qualified Data.Version as Version
import qualified Data.Word as Word
import qualified GHC.Generics as Generics
import qualified Text.ParserCombinators.ReadP as ReadP

-- $setup
--
-- >>> import Control.Applicative (Const(..))
-- >>> let view lens record = getConst (lens Const record)
--
-- >>> import Data.Functor.Identity (Identity(..))
-- >>> let set lens field record = runIdentity (lens (const (Identity field)) record)
--
-- >>> import Control.Applicative ((<$>), (<*>))

-- * Public

-- | A semantic version number. Versions have five parts:
--
-- 1. 'majorLens': The major version number.
-- 2. 'minorLens': The minor version number.
-- 3. 'patchLens': The patch version number.
-- 4. 'preReleasesLens': A list of pre-release identifiers.
-- 5. 'buildsLens': A list of build metadata.
--
-- Use 'parseVersion' to create versions.
data Version = Version
  { versionMajor :: Word.Word64
  , versionMinor :: Word.Word64
  , versionPatch :: Word.Word64
  , versionPreReleases :: [PreRelease]
  , versionBuilds :: [Build]
  } deriving (Data.Data, Eq, Generics.Generic, Read, Show)

-- | In general, versions compare in the way that you would expect. First the
-- major version numbers are compared, then the minors, then the patches.
--
-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "2.0.0"
-- Just LT
-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "1.3.0"
-- Just LT
-- >>> compare <$> parseVersion "1.2.3" <*> parseVersion "1.2.4"
-- Just LT
--
-- Numbers are compared numerically, not alphabetically.
--
-- >>> compare <$> parseVersion "0.0.9" <*> parseVersion "0.0.10"
-- Just LT
--
-- If all the numbers are the same, the pre-releases are compared.
--
-- >>> compare <$> parseVersion "1.2.3-a" <*> parseVersion "1.2.3-b"
-- Just LT
--
-- A version with a pre-release is always less than a version without one as
-- long as the other parts are the same.
--
-- >>> compare <$> parseVersion "1.2.3-pre" <*> parseVersion "1.2.3"
-- Just LT
-- >>> compare <$> parseVersion "1.2.4-pre" <*> parseVersion "1.2.3"
-- Just GT
--
-- Builds are not considered when comparing versions.
--
-- >>> compare <$> parseVersion "1.2.3+a" <*> parseVersion "1.2.3+b"
-- Just EQ
-- >>> (==) <$> parseVersion "1.2.3+a" <*> parseVersion "1.2.3+b"
-- Just False
instance Ord Version where
  compare x y = Monoid.mconcat
    [ Ord.comparing versionMajor x y
    , Ord.comparing versionMinor x y
    , Ord.comparing versionPatch x y
    , case both versionPreReleases (x, y) of
      ([], []) -> EQ
      ([], _) -> GT
      (_, []) -> LT
      (p, q) -> compare p q
    ]

-- | Pre-release information attached to a version. These can either be numeric
-- or textual. They must not be empty.
--
-- - Numeric: Can be any non-negative integer. Cannot have leading zeros.
--
-- - Textual: Can be any string of ASCII digits, letters, or hyphens. Cannot be
--   all digits, as that would be numeric.
--
-- In general, pre-releases must match the regular expression
-- @\/^[-0-9A-Za-z]+$\/@.
--
-- Use 'parsePreRelease' to create pre-releases.
data PreRelease
  = PreReleaseNumeric Word.Word64
  | PreReleaseTextual String
  deriving (Data.Data, Eq, Generics.Generic, Read, Show)

-- | Numeric pre-releases are always less than textual pre-releases.
--
-- >>> compare <$> parsePreRelease "1" <*> parsePreRelease "a"
-- Just LT
--
-- Numeric pre-releases are compared numerically.
--
-- >>> compare <$> parsePreRelease "9" <*> parsePreRelease "10"
-- Just LT
--
-- Textual pre-releases are compared alphabetically.
--
-- >>> compare <$> parsePreRelease "p10" <*> parsePreRelease "p9"
-- Just LT
instance Ord PreRelease where
  compare x y = case (x, y) of
    (PreReleaseNumeric n, PreReleaseNumeric m) -> compare n m
    (PreReleaseNumeric _, PreReleaseTextual _) -> LT
    (PreReleaseTextual _, PreReleaseNumeric _) -> GT
    (PreReleaseTextual s, PreReleaseTextual t) -> compare s t

-- | Build metadata attached to a version. These are similar to
-- 'PreRelease's with some key differences:
--
-- 1. There is no such thing as numeric builds. Even though builds can look
--    like numbers, all builds are textual.
-- 2. As a result, builds that look numeric are allowed to have leading zeros.
-- 3. Builds cannot be compared. That is, they do not have an 'Ord' instance.
--
-- Use 'parseBuild' to create builds.
newtype Build = Build String
  deriving (Data.Data, Eq, Generics.Generic, Read, Show)

-- | Constrains allowable version numbers.
--
-- Use 'parseConstraint' to create constraints and 'satisfiesConstraint' to see
-- if a version number satisfies a constraint.
data Constraint
  = ConstraintOperator Operator Version
  | ConstraintHyphen Version Version
  | ConstraintWildcard Wildcard
  | ConstraintAnd Constraint Constraint
  | ConstraintOr Constraint Constraint
  deriving (Data.Data, Eq, Generics.Generic, Ord, Read, Show)

-- | Makes a new version number.
--
-- >>> makeVersion 1 2 3 [unsafeParsePreRelease "pre"] [unsafeParseBuild "build"]
-- Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "pre"], versionBuilds = [Build "build"]}
--
-- This can be a useful alternative to 'parseVersion' if you want a total way
-- to create a version.
makeVersion :: Word.Word64 -> Word.Word64 -> Word.Word64 -> [PreRelease] -> [Build] -> Version
makeVersion major minor patch preReleases builds = Version
  { versionMajor = major
  , versionMinor = minor
  , versionPatch = patch
  , versionPreReleases = preReleases
  , versionBuilds = builds
  }

-- | The initial version number for development.
--
-- >>> initialVersion
-- Version {versionMajor = 0, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []}
initialVersion :: Version
initialVersion = makeVersion 0 0 0 [] []

-- | Attempts to parse a version. This parser follows
-- [SemVer's BNF](https://github.com/mojombo/semver/blob/eb9aac5/semver.md#backusnaur-form-grammar-for-valid-semver-versions).
--
-- >>> parseVersion "1.2.3-p.4+b.5"
-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "p",PreReleaseNumeric 4], versionBuilds = [Build "b",Build "5"]})
--
-- Returns 'Nothing' if the parse fails.
--
-- >>> parseVersion "wrong"
-- Nothing
--
-- Whitespace is allowed.
--
-- >>> parseVersion " 1.2.3 "
-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
parseVersion :: String -> Maybe Version
parseVersion s = parse
  (do
    ReadP.skipSpaces
    version <- versionP
    ReadP.skipSpaces
    return version)
  s

-- | Attempts to parse a pre-release.
--
-- >>> parsePreRelease "pre"
-- Just (PreReleaseTextual "pre")
-- >>> parsePreRelease "1"
-- Just (PreReleaseNumeric 1)
--
-- Returns 'Nothing' if the parse fails.
--
-- >>> parsePreRelease "wrong!"
-- Nothing
--
-- Numeric pre-releases cannot contain leading zeros.
--
-- >>> parsePreRelease "01"
-- Nothing
parsePreRelease :: String -> Maybe PreRelease
parsePreRelease s = parse preReleaseP s

-- | Attempts to parse a build.
--
-- >>> parseBuild "build"
-- Just (Build "build")
-- >>> parseBuild "1"
-- Just (Build "1")
--
-- Returns 'Nothing' if the parse fails.
--
-- >>> parseBuild "wrong!"
-- Nothing
--
-- Unlike pre-releases, numeric builds can have leading zeros.
--
-- >>> parseBuild "01"
-- Just (Build "01")
parseBuild :: String -> Maybe Build
parseBuild s = parse buildP s

-- | Attempts to parse a constraint. This parser mostly follows
-- [npm's BNF](https://github.com/npm/npm/blob/d081cc6/doc/misc/semver.md#range-grammar).
--
-- >>> parseConstraint ">1.2.3"
-- Just (ConstraintOperator OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
--
-- Returns 'Nothing' if the parse fails.
--
-- >>> parseConstraint "wrong"
-- Nothing
--
-- The two departures from npm's BNF are that x-ranges cannot be used with
-- other operators and partial version numbers are not allowed.
--
-- >>> parseConstraint "1.2.x"
-- Just (ConstraintWildcard (WildcardPatch 1 2))
-- >>> parseConstraint ">=1.2.x"
-- Nothing
--
-- >>> parseConstraint "1.2"
-- Nothing
-- >>> parseConstraint ">=1.2"
-- Nothing
parseConstraint :: String -> Maybe Constraint
parseConstraint s = parse constraintsP s

-- | Parses a version.
--
-- >>> unsafeParseVersion "1.2.3-p.4+b.5"
-- Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [PreReleaseTextual "p",PreReleaseNumeric 4], versionBuilds = [Build "b",Build "5"]}
--
-- Raises an exception if the parse fails.
--
-- >>> unsafeParseVersion "wrong"
-- *** Exception: unsafeParseVersion: invalid version: "wrong"
-- ...
--
-- See 'parseVersion' for a safe version of this function.
unsafeParseVersion :: String -> Version
unsafeParseVersion s = case parseVersion s of
  Nothing -> error ("unsafeParseVersion: invalid version: " ++ show s)
  Just v -> v

-- | Parses a pre-release.
--
-- >>> unsafeParsePreRelease "pre"
-- PreReleaseTextual "pre"
--
-- Raises an exception if the parse fails.
--
-- >>> unsafeParsePreRelease "wrong!"
-- *** Exception: unsafeParsePreRelease: invalid pre-release: "wrong!"
-- ...
--
-- See 'parsePreRelease' for a safe version of this function.
unsafeParsePreRelease :: String -> PreRelease
unsafeParsePreRelease s = case parsePreRelease s of
  Nothing -> error ("unsafeParsePreRelease: invalid pre-release: " ++ show s)
  Just p -> p

-- | Parses a build.
--
-- >>> unsafeParseBuild "build"
-- Build "build"
--
-- Raises an exception if the parse fails.
--
-- >>> unsafeParseBuild "wrong!"
-- Build "*** Exception: unsafeParseBuild: invalid build: "wrong!"
-- ...
--
-- See 'parseBuild' for a safe version of this function.
unsafeParseBuild :: String -> Build
unsafeParseBuild s = case parseBuild s of
  Nothing -> error ("unsafeParseBuild: invalid build: " ++ show s)
  Just b -> b

-- | Parses a constraint.
--
-- >>> unsafeParseConstraint ">1.2.3"
-- ConstraintOperator OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
--
-- Raises an exception if the parse fails.
--
-- >>> unsafeParseConstraint "wrong"
-- *** Exception: unsafeParseConstraint: invalid constraint: "wrong"
-- ...
--
-- See 'parseConstraint' for a safe version of this function.
unsafeParseConstraint :: String -> Constraint
unsafeParseConstraint s = case parseConstraint s of
  Nothing -> error ("unsafeParseConstraint: invalid constraint: " ++ show s)
  Just c -> c

-- | Renders a version.
--
-- >>> renderVersion <$> parseVersion "1.2.3-p.4+b.5"
-- Just "1.2.3-p.4+b.5"
renderVersion :: Version -> String
renderVersion v = concat
  [ show (versionMajor v)
  , "."
  , show (versionMinor v)
  , "."
  , show (versionPatch v)
  , renderPreReleases (versionPreReleases v)
  , renderBuilds (versionBuilds v)
  ]

-- | Renders a pre-release.
--
-- >>> renderPreRelease <$> parsePreRelease "pre"
-- Just "pre"
-- >>> renderPreRelease <$> parsePreRelease "1"
-- Just "1"
renderPreRelease :: PreRelease -> String
renderPreRelease p = case p of
  PreReleaseNumeric n -> show n
  PreReleaseTextual s -> s

-- | Renders a build.
--
-- >>> renderBuild <$> parseBuild "build"
-- Just "build"
-- >>> renderBuild <$> parseBuild "1"
-- Just "1"
renderBuild :: Build -> String
renderBuild (Build b) = b

-- | Renders a constraint.
--
-- >>> renderConstraint <$> parseConstraint ">1.2.3"
-- Just ">1.2.3"
--
-- Parsing and rendering a constraint doesn't always return what you started
-- with.
--
-- >>> renderConstraint <$> parseConstraint "=1.2.3"
-- Just "1.2.3"
renderConstraint :: Constraint -> String
renderConstraint c = case c of
  ConstraintOperator o v ->
    let s = renderVersion v
    in case o of
      OperatorLT -> '<' : s
      OperatorLE -> '<' : '=' : s
      OperatorEQ -> s
      OperatorGE -> '>' : '=' : s
      OperatorGT -> '>' : s
      OperatorTilde -> '~' : s
      OperatorCaret -> '^' : s
  ConstraintHyphen l r -> unwords [renderVersion l, "-", renderVersion r]
  ConstraintWildcard w -> case w of
    WildcardMajor -> "x.x.x"
    WildcardMinor m -> show m ++ ".x.x"
    WildcardPatch m n -> List.intercalate "." [show m, show n, "x"]
  ConstraintAnd l r -> unwords (map renderConstraint [l, r])
  ConstraintOr l r -> unwords [renderConstraint l, "||", renderConstraint r]

-- | Returns 'True' if the major version number is zero, 'False' otherwise.
--
-- >>> isUnstable <$> parseVersion "0.1.2"
-- Just True
-- >>> isUnstable <$> parseVersion "1.0.0"
-- Just False
isUnstable :: Version -> Bool
isUnstable v = versionMajor v == 0

-- | Returns 'True' if the major version number is not zero, 'False' otherwise.
--
-- >>> isStable <$> parseVersion "1.0.0"
-- Just True
-- >>> isStable <$> parseVersion "0.1.2"
-- Just False
isStable :: Version -> Bool
isStable v = not (isUnstable v)

-- | Converts from a 'Version.Version' from the @base@ package.
--
-- >>> renderVersion . fromBaseVersion $ Version.makeVersion [1, 2, 3]
-- "1.2.3"
--
-- Missing version components are set to zero.
--
-- >>> renderVersion . fromBaseVersion $ Version.makeVersion []
-- "0.0.0"
-- >>> renderVersion . fromBaseVersion $ Version.makeVersion [1]
-- "1.0.0"
-- >>> renderVersion . fromBaseVersion $ Version.makeVersion [1, 2]
-- "1.2.0"
--
-- Extra version components are ignored.
--
-- >>> renderVersion . fromBaseVersion $ Version.makeVersion [1, 2, 3, 4]
-- "1.2.3"
--
-- Tags are ignored.
--
-- >>> renderVersion . fromBaseVersion $ Version.Version [] ["ignored"]
-- "0.0.0"
fromBaseVersion :: Version.Version -> Version
fromBaseVersion v = case Version.versionBranch v of
  (m : n : p : _) -> mkV (fromIntegral m) (fromIntegral n) (fromIntegral p)
  (m : n : _) -> mkV (fromIntegral m) (fromIntegral n) 0
  (m : _) -> mkV (fromIntegral m) 0 0
  _ -> mkV 0 0 0

-- | Converts to a 'Version.Version' from the @base@ package.
--
-- >>> toBaseVersion <$> parseVersion "1.2.3"
-- Just (Version {versionBranch = [1,2,3], versionTags = []})
--
-- Pre-releases and builds are converted to tags.
--
-- >>> toBaseVersion <$> parseVersion "1.2.3-pre+build"
-- Just (Version {versionBranch = [1,2,3], versionTags = ["pre","build"]})
toBaseVersion :: Version -> Version.Version
toBaseVersion v = Version.Version
  (map fromIntegral
    [ versionMajor v
    , versionMinor v
    , versionPatch v
    ])
  (concat
    [ map renderPreRelease (versionPreReleases v)
    , map renderBuild (versionBuilds v)
    ])

-- | Increments the major version number.
--
-- >>> bumpMajor <$> parseVersion "0.0.0"
-- Just (Version {versionMajor = 1, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
--
-- The minor and patch numbers are reset to zero.
--
-- >>> bumpMajor <$> parseVersion "1.2.3"
-- Just (Version {versionMajor = 2, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
--
-- The pre-releases and builds are removed.
--
-- >>> bumpMajor <$> parseVersion "0.0.0-pre+build"
-- Just (Version {versionMajor = 1, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
--
-- Consider using 'majorLens' if you want to arbitrarily change the major
-- number, or if you don't want the other parts of the version to change.
bumpMajor :: Version -> Version
bumpMajor v = makeVersion (versionMajor v + 1) 0 0 [] []

-- | Increments the minor version number.
--
-- >>> bumpMinor <$> parseVersion "0.0.0"
-- Just (Version {versionMajor = 0, versionMinor = 1, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
--
-- The patch number is reset to zero.
--
-- >>> bumpMinor <$> parseVersion "1.2.3"
-- Just (Version {versionMajor = 1, versionMinor = 3, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
--
-- The pre-releases and builds are removed.
--
-- >>> bumpMinor <$> parseVersion "0.0.0-pre+build"
-- Just (Version {versionMajor = 0, versionMinor = 1, versionPatch = 0, versionPreReleases = [], versionBuilds = []})
--
-- Consider using 'minorLens' if you want to arbitrarily change the minor
-- number, or if you don't want the other parts of the version to change.
bumpMinor :: Version -> Version
bumpMinor v = makeVersion (versionMajor v) (versionMinor v + 1) 0 [] []

-- | Increments the patch number.
--
-- >>> bumpPatch <$> parseVersion "0.0.0"
-- Just (Version {versionMajor = 0, versionMinor = 0, versionPatch = 1, versionPreReleases = [], versionBuilds = []})
--
-- The major and minor numbers are not changed.
--
-- >>> bumpPatch <$> parseVersion "1.2.3"
-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 4, versionPreReleases = [], versionBuilds = []})
--
-- The pre-releases and builds are removed.
--
-- >>> bumpPatch <$> parseVersion "0.0.0-pre+build"
-- Just (Version {versionMajor = 0, versionMinor = 0, versionPatch = 1, versionPreReleases = [], versionBuilds = []})
--
-- Consider using 'patchLens' if you want to arbitrarily change the patch
-- number, or if you don't want the other parts of the version to change.
bumpPatch :: Version -> Version
bumpPatch v = makeVersion
  (versionMajor v) (versionMinor v) (versionPatch v + 1) [] []

-- | Returns 'True' if the version satisfies the constraint, 'False' otherwise.
--
-- >>> satisfiesConstraint <$> parseConstraint ">1.2.0" <*> parseVersion "1.2.3"
-- Just True
satisfiesConstraint :: Constraint -> Version -> Bool
satisfiesConstraint c v = satisfiesSC (toSC c) v

-- | Focuses on the major version number.
--
-- >>> view majorLens <$> parseVersion "1.2.3-pre.4+build.5"
-- Just 1
-- >>> set majorLens 4 <$> parseVersion "1.2.3"
-- Just (Version {versionMajor = 4, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
majorLens :: Functor f => (Word.Word64 -> f Word.Word64) -> Version -> f Version
majorLens f v = fmap
  (\ m -> v { versionMajor = m })
  (f (versionMajor v))

-- | Focuses on the minor version number.
--
-- >>> view minorLens <$> parseVersion "1.2.3-pre.4+build.5"
-- Just 2
-- >>> set minorLens 4 <$> parseVersion "1.2.3"
-- Just (Version {versionMajor = 1, versionMinor = 4, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
minorLens :: Functor f => (Word.Word64 -> f Word.Word64) -> Version -> f Version
minorLens f v = fmap
  (\ n -> v { versionMinor = n })
  (f (versionMinor v))

-- | Focuses on the patch version number.
--
-- >>> view patchLens <$> parseVersion "1.2.3-pre.4+build.5"
-- Just 3
-- >>> set patchLens 4 <$> parseVersion "1.2.3"
-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 4, versionPreReleases = [], versionBuilds = []})
patchLens :: Functor f => (Word.Word64 -> f Word.Word64) -> Version -> f Version
patchLens f v = fmap
  (\ p -> v { versionPatch = p })
  (f (versionPatch v))

-- | Focuses on the pre-release identifiers.
--
-- >>> view preReleasesLens <$> parseVersion "1.2.3-pre.4+build.5"
-- Just [PreReleaseTextual "pre",PreReleaseNumeric 4]
-- >>> set preReleasesLens [] <$> parseVersion "1.2.3-pre"
-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
preReleasesLens :: Functor f => ([PreRelease] -> f [PreRelease]) -> Version -> f Version
preReleasesLens f v = fmap
  (\ ps -> v { versionPreReleases = ps })
  (f (versionPreReleases v))

-- | Focuses on the build metadata.
--
-- >>> view buildsLens <$> parseVersion "1.2.3-pre.4+build.5"
-- Just [Build "build",Build "5"]
-- >>> set buildsLens [] <$> parseVersion "1.2.3+build"
-- Just (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})
buildsLens :: Functor f => ([Build] -> f [Build]) -> Version -> f Version
buildsLens f v = fmap
  (\ bs -> v { versionBuilds = bs })
  (f (versionBuilds v))

-- * Private

-- ** Types

data Operator
  = OperatorLT
  | OperatorLE
  | OperatorEQ
  | OperatorGE
  | OperatorGT
  | OperatorTilde
  | OperatorCaret
  deriving (Data.Data, Eq, Generics.Generic, Ord, Read, Show)

data Wildcard
  = WildcardMajor
  | WildcardMinor Word.Word64
  | WildcardPatch Word.Word64 Word.Word64
  deriving (Data.Data, Eq, Generics.Generic, Ord, Read, Show)

-- ** Constructors

-- | Makes a new constraint that must be less than the version number.
--
-- >>> constraintLT <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorLT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint "<1.2.3"
-- Just (ConstraintOperator OperatorLT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintLT :: Version -> Constraint
constraintLT v = ConstraintOperator OperatorLT v

-- | Makes a new constraint that must be less than or euqal to the version
-- number.
--
-- >>> constraintLE <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorLE (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint "<=1.2.3"
-- Just (ConstraintOperator OperatorLE (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintLE :: Version -> Constraint
constraintLE v = ConstraintOperator OperatorLE v

-- | Makes a new constraint that must be equal to the version number.
--
-- >>> constraintEQ <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorEQ (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint "=1.2.3"
-- Just (ConstraintOperator OperatorEQ (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintEQ :: Version -> Constraint
constraintEQ v = ConstraintOperator OperatorEQ v

-- | Makes a new constraint that must be greater than or equal to the version
-- number.
--
-- >>> constraintGE <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorGE (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint ">=1.2.3"
-- Just (ConstraintOperator OperatorGE (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintGE :: Version -> Constraint
constraintGE v = ConstraintOperator OperatorGE v

-- | Makes a new constraint that must be greater than the version number.
--
-- >>> constraintGT <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint ">1.2.3"
-- Just (ConstraintOperator OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintGT :: Version -> Constraint
constraintGT v = ConstraintOperator OperatorGT v

-- | Makes a new constraint that must satisfy both constraints.
--
-- >>> constraintAnd <$> (constraintGE <$> parseVersion "1.2.3") <*> (constraintLT <$> parseVersion "2.0.0")
-- Just (ConstraintAnd (ConstraintOperator OperatorGE (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})) (ConstraintOperator OperatorLT (Version {versionMajor = 2, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})))
-- >>> parseConstraint ">=1.2.3 <2.0.0"
-- Just (ConstraintAnd (ConstraintOperator OperatorGE (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})) (ConstraintOperator OperatorLT (Version {versionMajor = 2, versionMinor = 0, versionPatch = 0, versionPreReleases = [], versionBuilds = []})))
constraintAnd :: Constraint -> Constraint -> Constraint
constraintAnd l r = ConstraintAnd l r

-- | Makes a new constraint that must satisfy either constraint.
--
-- >>> constraintOr <$> (constraintEQ <$> parseVersion "1.2.3") <*> (constraintGT <$> parseVersion "1.2.3")
-- Just (ConstraintOr (ConstraintOperator OperatorEQ (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})) (ConstraintOperator OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})))
-- >>> parseConstraint "=1.2.3 || >1.2.3"
-- Just (ConstraintOr (ConstraintOperator OperatorEQ (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})) (ConstraintOperator OperatorGT (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []})))
constraintOr :: Constraint -> Constraint -> Constraint
constraintOr l r = ConstraintOr l r

-- | Makes a new constraint that must be between the versions, inclusive.
--
-- >>> constraintHyphen <$> parseVersion "1.2.3" <*> parseVersion "2.3.4"
-- Just (ConstraintHyphen (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}) (Version {versionMajor = 2, versionMinor = 3, versionPatch = 4, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint "1.2.3 - 2.3.4"
-- Just (ConstraintHyphen (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}) (Version {versionMajor = 2, versionMinor = 3, versionPatch = 4, versionPreReleases = [], versionBuilds = []}))
constraintHyphen :: Version -> Version -> Constraint
constraintHyphen v w = ConstraintHyphen v w

-- | Makes a new constraint that allows changes to the patch version number.
--
-- >>> constraintTilde <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorTilde (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint "~1.2.3"
-- Just (ConstraintOperator OperatorTilde (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintTilde :: Version -> Constraint
constraintTilde v = ConstraintOperator OperatorTilde v

-- | Makes a new constraint that allows changes that do not modify the
-- left-most non-zero version number.
--
-- >>> constraintCaret <$> parseVersion "1.2.3"
-- Just (ConstraintOperator OperatorCaret (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
-- >>> parseConstraint "^1.2.3"
-- Just (ConstraintOperator OperatorCaret (Version {versionMajor = 1, versionMinor = 2, versionPatch = 3, versionPreReleases = [], versionBuilds = []}))
constraintCaret :: Version -> Constraint
constraintCaret v = ConstraintOperator OperatorCaret v

-- ** Parsing

versionP :: ReadP.ReadP Version
versionP = do
  major <- numberP
  Monad.void (ReadP.char '.')
  minor <- numberP
  Monad.void (ReadP.char '.')
  patch <- numberP
  preReleases <- preReleasesP
  builds <- buildsP
  return (makeVersion major minor patch preReleases builds)

preReleasesP :: ReadP.ReadP [PreRelease]
preReleasesP = ReadP.option [] (do
  Monad.void (ReadP.char '-')
  ReadP.sepBy1 preReleaseP (ReadP.char '.'))

preReleaseP :: ReadP.ReadP PreRelease
preReleaseP = preReleaseNumberP ReadP.<++ preReleaseStringP

preReleaseNumberP :: ReadP.ReadP PreRelease
preReleaseNumberP = do
  n <- numberP
  return (PreReleaseNumeric n)

preReleaseStringP :: ReadP.ReadP PreRelease
preReleaseStringP = do
  s <- ReadP.munch1 isIdentifier
  if all Char.isDigit s
    then ReadP.pfail
    else return (PreReleaseTextual s)

buildsP :: ReadP.ReadP [Build]
buildsP = ReadP.option [] (do
  Monad.void (ReadP.char '+')
  ReadP.sepBy1 buildP (ReadP.char '.'))

buildP :: ReadP.ReadP Build
buildP = do
  b <- ReadP.munch1 isIdentifier
  return (Build b)

numberP :: ReadP.ReadP Word.Word64
numberP = zeroP ReadP.<++ nonZeroP

zeroP :: ReadP.ReadP Word.Word64
zeroP = do
  Monad.void (ReadP.char '0')
  return 0

nonZeroP :: ReadP.ReadP Word.Word64
nonZeroP = do
  x <- ReadP.satisfy isAsciiDigitNonZero
  ys <- ReadP.munch Char.isDigit
  case toWord64 (stringToIntegral (x : ys)) of
    Nothing -> ReadP.pfail
    Just n -> return n

constraintsP :: ReadP.ReadP Constraint
constraintsP = do
  spacesP
  cs <- ReadP.sepBy1 constraintP orP
  spacesP
  return (foldr1 constraintOr cs)

constraintP :: ReadP.ReadP Constraint
constraintP = do
  cs <- ReadP.sepBy1 simpleP spaces1P
  return (foldr1 constraintAnd cs)

hyphenatedP :: ReadP.ReadP Constraint
hyphenatedP = do
  v <- versionP
  hyphenP
  w <- versionP
  return (constraintHyphen v w)

simpleP :: ReadP.ReadP Constraint
simpleP = ReadP.choice [hyphenatedP, wildcardConstraintP, primitiveP]

wildcardConstraintP :: ReadP.ReadP Constraint
wildcardConstraintP = do
  ReadP.optional (ReadP.char '=')
  w <- wildcardP
  return (ConstraintWildcard w)

wildcardP :: ReadP.ReadP Wildcard
wildcardP = ReadP.choice [wildcardPatchP, wildcardMinorP, wildcardMajorP]

wildcardPatchP :: ReadP.ReadP Wildcard
wildcardPatchP = do
  m <- numberP
  Monad.void (ReadP.char '.')
  n <- numberP
  Monad.void (ReadP.char '.')
  Monad.void (ReadP.satisfy isWildcard)
  return (WildcardPatch m n)

wildcardMinorP :: ReadP.ReadP Wildcard
wildcardMinorP = do
  m <- numberP
  Monad.void (ReadP.char '.')
  Monad.void (ReadP.satisfy isWildcard)
  Monad.void (ReadP.char '.')
  Monad.void (ReadP.satisfy isWildcard)
  return (WildcardMinor m)

wildcardMajorP :: ReadP.ReadP Wildcard
wildcardMajorP = do
  Monad.void (ReadP.satisfy isWildcard)
  Monad.void (ReadP.char '.')
  Monad.void (ReadP.satisfy isWildcard)
  Monad.void (ReadP.char '.')
  Monad.void (ReadP.satisfy isWildcard)
  return WildcardMajor

primitiveP :: ReadP.ReadP Constraint
primitiveP = do
  o <- operatorP
  spacesP
  v <- versionP
  return (ConstraintOperator o v)

operatorP :: ReadP.ReadP Operator
operatorP = ReadP.choice
  [ ReadP.string "<=" >> return OperatorLE
  , ReadP.string ">=" >> return OperatorGE
  , ReadP.char '<' >> return OperatorLT
  , ReadP.char '>' >> return OperatorGT
  , ReadP.char '=' >> return OperatorEQ
  , ReadP.char '^' >> return OperatorCaret
  , ReadP.char '~' >> return OperatorTilde
  , return OperatorEQ
  ]

hyphenP :: ReadP.ReadP ()
hyphenP = do
  spaces1P
  Monad.void (ReadP.char '-')
  spaces1P

orP :: ReadP.ReadP ()
orP = do
  spaces1P
  Monad.void (ReadP.string "||")
  spaces1P

spaces1P :: ReadP.ReadP ()
spaces1P = Monad.void (ReadP.munch1 (== ' '))

spacesP :: ReadP.ReadP ()
spacesP = Monad.void (ReadP.munch (== ' '))

-- ** Rendering

renderPreReleases :: [PreRelease] -> String
renderPreReleases ps = if null ps
  then ""
  else '-' : List.intercalate "." (map renderPreRelease ps)

renderBuilds :: [Build] -> String
renderBuilds bs = if null bs
  then ""
  else '+' : List.intercalate "." (map renderBuild bs)

-- ** Helpers

both :: (a -> b) -> (a, a) -> (b, b)
both f (x, y) = (f x, f y)

isAsciiDigitNonZero :: Char -> Bool
isAsciiDigitNonZero c = Char.isDigit c && (c /= '0')

isIdentifier :: Char -> Bool
isIdentifier c = (Char.isAscii c && Char.isAlphaNum c) || (c == '-')

isWildcard :: Char -> Bool
isWildcard c = (c == 'x') || (c == '*') || (c == 'X')

parse :: ReadP.ReadP a -> String -> Maybe a
parse p s =
  let p' = ReadP.readP_to_S p
  in Maybe.listToMaybe (do
    (x, "") <- p' s
    return x)

stringToIntegral :: Integral a => String -> a
stringToIntegral s = foldl
  (\ n d -> (n * 10) + (fromIntegral (fromEnum d) - 48)) 0 s

toWord64 :: Integer -> Maybe Word.Word64
toWord64 n =
  if n < 0
  then Nothing
  else if n > fromIntegral (maxBound :: Word.Word64)
  then Nothing
  else Just (fromIntegral n)

-- * Simple constraints
-- | Simple constraints are just as expressive as 'Constraint's, but they are
-- easier to reason about. You can think of them as the desugared version of
-- 'Constraint's.

data SimpleConstraint
  = SCLT Version
  | SCEQ Version
  | SCGT Version
  | SCAnd SimpleConstraint SimpleConstraint
  | SCOr SimpleConstraint SimpleConstraint
  deriving (Data.Data, Eq, Generics.Generic, Ord, Read, Show)

mkV :: Word.Word64 -> Word.Word64 -> Word.Word64 -> Version
mkV m n p = makeVersion m n p [] []

satisfiesSC :: SimpleConstraint -> Version -> Bool
satisfiesSC c v = case c of
  SCLT u -> v < u
  -- This uses `compare` rather than `==` to ignore build metadata.
  SCEQ u -> compare v u == EQ
  SCGT u -> v > u
  SCAnd l r -> satisfiesSC l v && satisfiesSC r v
  SCOr l r -> satisfiesSC l v || satisfiesSC r v

scLE :: Version -> SimpleConstraint
scLE v = SCOr (SCLT v) (SCEQ v)

scGE :: Version -> SimpleConstraint
scGE v = SCOr (SCGT v) (SCEQ v)

toSC :: Constraint -> SimpleConstraint
toSC c = case c of
  ConstraintOperator o v -> case o of
    OperatorLT -> SCLT v
    OperatorLE -> scLE v
    OperatorEQ -> SCEQ v
    OperatorGE -> scGE v
    OperatorGT -> SCGT v
    OperatorTilde -> SCAnd
      (scGE v)
      (SCLT (mkV (versionMajor v) (versionMinor v + 1) 0))
    OperatorCaret -> SCAnd
      (scGE v)
      (SCLT (case (versionMajor v, versionMinor v, versionPatch v) of
        (0, 0, p) -> mkV 0 0 (p + 1)
        (0, n, _) -> mkV 0 (n + 1) 0
        (m, _, _) -> mkV (m + 1) 0 0))
  ConstraintHyphen l h -> SCAnd (scGE l) (scLE h)
  ConstraintWildcard w -> case w of
    WildcardMajor -> scGE initialVersion
    WildcardMinor m -> SCAnd (scGE (mkV m 0 0)) (SCLT (mkV (m + 1) 0 0))
    WildcardPatch m n -> SCAnd (scGE (mkV m n 0)) (SCLT (mkV m (n + 1) 0))
  ConstraintAnd l r -> SCAnd (toSC l) (toSC r)
  ConstraintOr l r -> SCOr (toSC l) (toSC r)