{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}
module Hpack.Syntax.DependencyVersion (
  githubBaseUrl
, GitRef
, GitUrl
, DependencyVersion(..)
, SourceDependency(..)
, dependencyVersionFromCabal

, scientificToVersion
, cabalParse
) where

import           Control.Applicative
import           Data.Scientific
import qualified Data.Text as T
import           Text.PrettyPrint (renderStyle, Style(..), Mode(..))

import           Distribution.Version (VersionRangeF(..))
import qualified Distribution.Text as D
import qualified Distribution.Version as D
import qualified Distribution.Parsec.Class as D

import           Data.Aeson.Config.FromValue

githubBaseUrl :: String
githubBaseUrl = "https://github.com/"

type GitUrl = String
type GitRef = String

data DependencyVersion =
    AnyVersion
  | VersionRange String
  | SourceDependency SourceDependency
  deriving (Eq, Show)

instance FromValue DependencyVersion where
  fromValue v = case v of
    Null -> return AnyVersion
    Object _ -> SourceDependency <$> fromValue v
    Number n -> return (scientificToDependencyVersion n)
    String s -> parseVersionRange ("== " ++ input) <|> parseVersionRange input
      where
        input = T.unpack s

    _ -> typeMismatch "Null, Object, Number, or String" v

data SourceDependency = GitRef GitUrl GitRef (Maybe FilePath) | Local FilePath
  deriving (Eq, Show)

instance FromValue SourceDependency where
  fromValue = withObject (\o -> let
    local :: Parser SourceDependency
    local = Local <$> o .: "path"

    git :: Parser SourceDependency
    git = GitRef <$> url <*> ref <*> subdir

    url :: Parser String
    url =
          ((githubBaseUrl ++) <$> o .: "github")
      <|> (o .: "git")
      <|> fail "neither key \"git\" nor key \"github\" present"

    ref :: Parser String
    ref = o .: "ref"

    subdir :: Parser (Maybe FilePath)
    subdir = o .:? "subdir"

    in local <|> git)

scientificToDependencyVersion :: Scientific -> DependencyVersion
scientificToDependencyVersion n = VersionRange ("==" ++ version)
  where
    version = scientificToVersion n

scientificToVersion :: Scientific -> String
scientificToVersion n = version
  where
    version = formatScientific Fixed (Just decimalPlaces) n
    decimalPlaces
      | e < 0 = abs e
      | otherwise = 0
    e = base10Exponent n

parseVersionRange :: Monad m => String -> m DependencyVersion
parseVersionRange = fmap dependencyVersionFromCabal . parseCabalVersionRange

parseCabalVersionRange :: Monad m => String -> m D.VersionRange
parseCabalVersionRange = cabalParse "constraint"

cabalParse :: (Monad m, D.Parsec a) => String -> String -> m a
cabalParse subject s = case D.eitherParsec s of
  Right d -> return d
  Left _ ->fail $ unwords ["invalid",  subject, show s]

dependencyVersionFromCabal :: D.VersionRange -> DependencyVersion
dependencyVersionFromCabal versionRange
  | D.isAnyVersion versionRange = AnyVersion
  | otherwise = VersionRange . renderStyle style . D.disp $ toPreCabal2VersionRange versionRange
  where
    style = Style OneLineMode 0 0

    toPreCabal2VersionRange :: D.VersionRange -> D.VersionRange
    toPreCabal2VersionRange = D.embedVersionRange . D.cataVersionRange f
      where
        f :: VersionRangeF (VersionRangeF D.VersionRange) -> VersionRangeF D.VersionRange
        f = \ case
          MajorBoundVersionF v -> IntersectVersionRangesF (D.embedVersionRange lower) (D.embedVersionRange upper)
            where
              lower = OrLaterVersionF v
              upper = EarlierVersionF (D.majorUpperBound v)

          AnyVersionF -> AnyVersionF
          ThisVersionF v -> ThisVersionF v
          LaterVersionF v -> LaterVersionF v
          OrLaterVersionF v -> OrLaterVersionF v
          EarlierVersionF v -> EarlierVersionF v
          OrEarlierVersionF v -> OrEarlierVersionF v
          WildcardVersionF v -> WildcardVersionF v
          UnionVersionRangesF a b -> UnionVersionRangesF (D.embedVersionRange a) (D.embedVersionRange b)
          IntersectVersionRangesF a b -> IntersectVersionRangesF (D.embedVersionRange a) (D.embedVersionRange b)
          VersionRangeParensF a -> VersionRangeParensF (D.embedVersionRange a)