{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}

module Data.SemVer.Parser (
    parseSemVer, parseSemVerRange, pSemVerRange, pSemVer,
    fromHaskellVersion, matchText, splitWS
  ) where

import qualified Prelude as P
import ClassyPrelude hiding (try, many)
import Text.Parsec hiding ((<|>), spaces, parse, State, uncons, optional)
import qualified Text.Parsec as Parsec
import qualified Data.Text as T
import Text.Read (readMaybe)

import Data.Version (Version(..))
import Data.SemVer.Types

type Parser = ParsecT String () Identity

-- | Split a text on whitespace. Why isn't this in the stdlib.
splitWS :: Text -> [Text]
splitWS = filter (/= "") . T.split (flip elem (" \t\n\r" :: String))

-------------------------------------------------------------------------------
-- Wildcards: intermediate representations of semvers
--
-- | A partially specified semantic version. Implicitly defines
-- a range of acceptable versions, as seen in @wildcardToRange@.
data Wildcard = Any
              | Maj Int
              | Min Int Int
              | Full SemVer
              deriving (Show, Eq)

-- | Fills in zeros in a wildcard.
wildcardToSemver :: Wildcard -> SemVer
wildcardToSemver Any = semver 0 0 0
wildcardToSemver (Maj n) = semver n 0 0
wildcardToSemver (Min n m) = semver n m 0
wildcardToSemver (Full sv) = sv

-- | Translates a wildcard (partially specified version) to a range.
-- Ex: 2 := >=2.0.0 <3.0.0
-- Ex: 1.2.x := 1.2 := >=1.2.0 <1.3.0
wildcardToRange :: Wildcard -> SemVerRange
wildcardToRange = \case
  Any -> Geq $ semver 0 0 0
  Maj n -> Geq (semver n 0 0) `And` Lt (semver (n+1) 0 0)
  Min n m -> Geq (semver n m 0) `And` Lt (semver n (m+1) 0)
  Full sv -> Eq sv

-- | Translates a ~wildcard to a range.
-- Ex: ~1.2.3 := >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0
tildeToRange :: Wildcard -> SemVerRange
tildeToRange = \case
  -- I'm not sure this is officially supported, but just in case...
  Any -> tildeToRange (Full $ semver 0 0 0)
  -- ~1 := >=1.0.0 <(1+1).0.0 := >=1.0.0 <2.0.0 (Same as 1.x)
  Maj n -> Geq (semver n 0 0) `And` Lt (semver (n+1) 0 0)
  -- ~1.2 := >=1.2.0 <1.(2+1).0 := >=1.2.0 <1.3.0 (Same as 1.2.x)
  Min n m -> Geq (semver n m 0) `And` Lt (semver n (m+1) 0)
  -- ~1.2.3 := >=1.2.3 <1.(2+1).0 := >=1.2.3 <1.3.0
  Full (SemVer n m o [] _) -> Geq (semver n m o) `And` Lt (semver n (m+1) 0)
  -- ~1.2.3-beta.2 := >=1.2.3-beta.2 <1.3.0
  Full (SemVer n m o tags _) -> Geq (semver' n m o tags) `And` Lt (semver n (m+1) 0)

-- | Translates a ^wildcard to a range.
-- Ex: ^1.2.x := >=1.2.0 <2.0.0
caratToRange :: Wildcard -> SemVerRange
caratToRange = \case
  Maj n -> Geq (semver n 0 0) `And` Lt (semver (n+1) 0 0)
  Min n m -> Geq (semver n m 0) `And` Lt (semver (n+1) 0 0)
  Full (SemVer 0 n m tags _) -> Geq (semver' 0 n m tags) `And` Lt (semver' 0 (n+1) 0 tags)
  Full (SemVer n m o tags _) -> Geq (semver' n m o tags) `And` Lt (semver' (n+1) 0 0 tags)

-- | Translates two hyphenated wildcards to an actual range.
-- Ex: 1.2.3 - 2.3.4 := >=1.2.3 <=2.3.4
-- Ex: 1.2 - 2.3.4 := >=1.2.0 <=2.3.4
-- Ex: 1.2.3 - 2 := >=1.2.3 <3.0.0
hyphenatedRange :: Wildcard -> Wildcard -> SemVerRange
hyphenatedRange wc1 wc2 = And sv1 sv2 where
  sv1 = case wc1 of Any -> anyVersion
                    Maj n -> Geq (semver n 0 0)
                    Min n m -> Geq (semver n m 0)
                    Full sv -> Geq sv
  sv2 = case wc2 of Any -> anyVersion
                    Maj n -> Lt (semver (n+1) 0 0)
                    Min n m -> Lt (semver n (m+1) 0)
                    Full sv -> Lt sv

-- | Given a parser and a string, attempts to parse the string.
parse :: Parser a -> Text -> Either ParseError a
parse p = Parsec.parse p "" . unpack

parseFull :: Parser a -> Text -> Either ParseError a
parseFull p = Parsec.parse (p <* eof) "" . unpack

-- | Consumes any spaces (not other whitespace).
spaces :: Parser String
spaces = many $ oneOf [' ', '\t', '\n', '\r']

-- | Consumes at least one space (not other whitespace).
spaces1 :: Parser String
spaces1 = many1 $ oneOf [' ', '\t', '\n', '\r']

-- | Parses the given string and any trailing spaces.
sstring :: String -> Parser String
sstring = lexeme . string

-- | Parses the given character and any trailing spaces.
schar :: Char -> Parser Char
schar = lexeme . char

-- | Parses `p` and any trailing spaces.
lexeme :: Parser a -> Parser a
lexeme p = p <* spaces

-- | Parses an integer.
pInt :: Parser Int
pInt = lexeme pInt'

-- | Parses an integer without consuming trailing spaces.
pInt' :: Parser Int
pInt' = P.read <$> many1 digit

-- | Parse a string as a version range, or return an error.
parseSemVerRange :: Text -> Either ParseError SemVerRange
parseSemVerRange text = case T.strip text of
  -- Handle a few special cases
  "" -> return anyVersion
  "||" -> return anyVersion
  t -> parse (pSemVerRange <* eof) t

-- | Parse a string as an explicit version, or return an error.
parseSemVer :: Text -> Either ParseError SemVer
parseSemVer = parse pSemVer . T.strip

-- | Parses a semantic version.
pSemVer :: Parser SemVer
pSemVer = do
  optional (char '=')
  wildcardToSemver <$> pWildCard

pVersionComp :: Parser SemVerRange
pVersionComp = cmp >>= \case
  "=" -> wildcardToRange <$> pWildCard
  "==" -> wildcardToRange <$> pWildCard
  -- This is a special case to deal with a test case in the npm semver
  -- test suite. The case states that "0.7.2" should satisfy
  -- "<=0.7.x". I'm interpreting this to mean that "<= X", where X is
  -- a range, means "less than or equal to the maximum supported in
  -- this range."
  "<=" -> Leq . topOf <$> pWildCard
  ">=" -> Geq <$> pSemVer
  ">" -> Gt <$> pSemVer
  "<" -> Lt <$> pSemVer
  where
    topOf = \case
      Any -> semver 0 0 0
      Maj n -> semver (n+1) 0 0
      Min n m -> semver n (m+1) 0
      Full sv -> sv

-- | Parses a comparison operator.
cmp :: Parser String
cmp = choice (try . sstring <$> [">=", "<=", ">", "<", "==", "="])

-- | Parses versions with an explicit range qualifier (gt, lt, etc).
pSemVerRangeSingle :: Parser SemVerRange
pSemVerRangeSingle = choice [
    wildcardToRange <$> pWildCard,
    pTildeRange,
    pCaratRange,
    pVersionComp
  ]

-- | Parses semantic version ranges joined with Ands and Ors.
pJoinedSemVerRange :: Parser SemVerRange
pJoinedSemVerRange = do
  first <- pSemVerRangeSingle
  option first $ do
    let next = choice [sstring "||", sstring "&&", map singleton anyChar]
    lookAhead next >>= \case
      "||" -> Or first <$> (sstring "||" *> pJoinedSemVerRange)
      "&&" -> And first <$> (sstring "&&" *> pJoinedSemVerRange)
      _ -> And first <$> pJoinedSemVerRange

-- | Parses a hyphenated range.
pHyphen :: Parser SemVerRange
pHyphen = hyphenatedRange <$> pWildCard <*> (sstring "-" *> pWildCard)

-- | Parses a "wildcard" (which is a possibly partial semantic version).
pWildCard :: Parser Wildcard
pWildCard = try $ do
  let seps = choice $ map string ["x", "X", "*"]
  let bound = choice [seps *> pure Nothing, Just <$> pInt']
  let getTag t = case readMaybe t of
        Just i -> IntTag i
        _ -> TextTag $ pack t
  let tag = getTag <$> many1 (letter <|> digit <|> char '-')
  -- Versions can optionally start with the character 'v'; ignore this.
  optional (char 'v')
  res <- takeWhile isJust <$> sepBy1 bound (sstring ".") >>= \case
    [] -> return Any
    [Just n] -> return $ Maj n
    [Just n, Just m] -> return $ Min n m
    [Just n, Just m, Just o] -> option (Full $ semver n m o) $ do
      tags <- option [] $ do
        -- Release tags might be separated by a hyphen, or not.
        optional (char '-')
        PrereleaseTags <$> (tag `sepBy1` char '.')
      -- Grab metadata if there is any
      metadata <- option [] $ do
        char '+'
        many1 (letter <|> digit <|> char '-') `sepBy1` char '.'
      return $ Full $ semver'' n m o tags (map pack metadata)
    w -> unexpected ("Invalid version " ++ show w)
  spaces *> return res

-- | Parses a tilde range (~1.2.3).
pTildeRange :: Parser SemVerRange
pTildeRange = do
  sstring "~"
  -- For some reason, including the following operators after
  -- a tilde is valid, but seems to have no effect.
  optional $ choice [try $ sstring ">=", sstring ">", sstring "="]
  tildeToRange <$> pWildCard

-- | Parses a carat range (^1.2.3).
pCaratRange :: Parser SemVerRange
pCaratRange = sstring "^" *> map caratToRange pWildCard

-- | Top-level parser. Parses a semantic version range.
pSemVerRange :: Parser SemVerRange
pSemVerRange = try pHyphen <|> pJoinedSemVerRange

-- | Parse a semver from a haskell version. There must be exactly
-- three numbers in the versionBranch field.
fromHaskellVersion :: Version -> Either Text SemVer
fromHaskellVersion v = case versionBranch v of
  [x, y, z] -> return (semver x y z) -- ignoring version tags since deprecated
  bad -> do
    let badVer = intercalate "." (map show bad)
    Left $ pack ("Not a SemVer version: " <> badVer)

-- | Parses the first argument as a range and the second argument as a semver,
-- and returns whether they match.
matchText :: Text -> Text -> Either Text Bool
matchText rangeTxt verTxt = case parseSemVerRange rangeTxt of
  Left err -> Left ("Could not parse range: " <> pack (show err))
  Right range -> case parseSemVer verTxt of
    Left err -> Left ("Could not parse version: " <> pack (show err))
    Right version -> Right $ matches range version
