{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}

module Stack.Types.Resolver
  (Resolver
  ,IsLoaded(..)
  ,LoadedResolver
  ,ResolverThat's(..)
  ,parseResolverText
  ,resolverDirName
  ,resolverName
  ,customResolverHash
  ,toResolverNotLoaded
  ,AbstractResolver(..)
  ,readAbstractResolver
  ) where

import           Control.Applicative
import           Control.Monad.Catch (MonadThrow, throwM)
import           Data.Aeson.Extended
                 (ToJSON, toJSON, FromJSON, parseJSON, object,
                  WithJSONWarnings(..), Value(String, Object), (.=),
                  noJSONWarnings, (..:), withObjectWarnings)
import           Data.Monoid.Extra
import           Data.Text (Text)
import qualified Data.Text as T
import           Data.Text.Encoding (decodeUtf8)
import           Data.Text.Read (decimal)
import           Options.Applicative (ReadM)
import qualified Options.Applicative as OA
import qualified Options.Applicative.Types as OA
import           Prelude
import           Stack.Types.BuildPlan (parseSnapName, renderSnapName, SnapName, SnapshotHash,
                                        trimmedSnapshotHash)
import           {-# SOURCE #-} Stack.Types.Config (ConfigException(..))
import           Stack.Types.Compiler

data IsLoaded = Loaded | NotLoaded

type LoadedResolver = ResolverThat's 'Loaded
type Resolver = ResolverThat's 'NotLoaded

-- TODO: once GHC 8.0 is the lowest version we support, make these into
-- actual haddock comments...

-- | How we resolve which dependencies to install given a set of packages.
data ResolverThat's (l :: IsLoaded) where
    -- Use an official snapshot from the Stackage project, either an LTS
    -- Haskell or Stackage Nightly.
    ResolverSnapshot :: !SnapName -> ResolverThat's l
    -- Require a specific compiler version, but otherwise provide no
    -- build plan. Intended for use cases where end user wishes to
    -- specify all upstream dependencies manually, such as using a
    -- dependency solver.
    ResolverCompiler :: !CompilerVersion -> ResolverThat's l
    -- A custom resolver based on the given name and URL. When a URL is
    -- provided, its contents must be completely immutable. Filepaths are
    -- always loaded. This constructor is used before the build-plan has
    -- been loaded, as we do not yet know the custom snapshot's hash.
    ResolverCustom :: !Text -> !Text -> ResolverThat's 'NotLoaded
    -- Like 'ResolverCustom', but after loading the build-plan, so we
    -- have a hash. This is necessary in order to identify the location
    -- files are stored for the resolver.
    ResolverCustomLoaded :: !Text -> !Text -> !SnapshotHash -> ResolverThat's 'Loaded

deriving instance Eq (ResolverThat's k)
deriving instance Show (ResolverThat's k)

instance ToJSON (ResolverThat's k) where
    toJSON x = case x of
        ResolverSnapshot{} -> toJSON $ resolverName x
        ResolverCompiler{} -> toJSON $ resolverName x
        ResolverCustom n l -> handleCustom n l
        ResolverCustomLoaded n l _ -> handleCustom n l
      where
        handleCustom n l = object
             [ "name" .= n
             , "location" .= l
             ]
instance FromJSON (WithJSONWarnings (ResolverThat's 'NotLoaded)) where
    -- Strange structuring is to give consistent error messages
    parseJSON v@(Object _) = withObjectWarnings "Resolver" (\o -> ResolverCustom
        <$> o ..: "name"
        <*> o ..: "location") v

    parseJSON (String t) = either (fail . show) return (noJSONWarnings <$> parseResolverText t)

    parseJSON _ = fail "Invalid Resolver, must be Object or String"

-- | Convert a Resolver into its @Text@ representation, as will be used by
-- directory names
resolverDirName :: LoadedResolver -> Text
resolverDirName (ResolverSnapshot name) = renderSnapName name
resolverDirName (ResolverCompiler v) = compilerVersionText v
resolverDirName (ResolverCustomLoaded name _ hash) = "custom-" <> name <> "-" <> decodeUtf8 (trimmedSnapshotHash hash)

-- | Convert a Resolver into its @Text@ representation for human
-- presentation.
resolverName :: ResolverThat's l -> Text
resolverName (ResolverSnapshot name) = renderSnapName name
resolverName (ResolverCompiler v) = compilerVersionText v
resolverName (ResolverCustom name _) = "custom-" <> name
resolverName (ResolverCustomLoaded name _ _) = "custom-" <> name

customResolverHash :: LoadedResolver-> Maybe SnapshotHash
customResolverHash (ResolverCustomLoaded _ _ hash) = Just hash
customResolverHash _ = Nothing

-- | Try to parse a @Resolver@ from a @Text@. Won't work for complex resolvers (like custom).
parseResolverText :: MonadThrow m => Text -> m Resolver
parseResolverText t
    | Right x <- parseSnapName t = return $ ResolverSnapshot x
    | Just v <- parseCompilerVersion t = return $ ResolverCompiler v
    | otherwise = throwM $ ParseResolverException t

toResolverNotLoaded :: LoadedResolver -> Resolver
toResolverNotLoaded r = case r of
    ResolverSnapshot s -> ResolverSnapshot s
    ResolverCompiler v -> ResolverCompiler v
    ResolverCustomLoaded n l _ -> ResolverCustom n l

-- | Either an actual resolver value, or an abstract description of one (e.g.,
-- latest nightly).
data AbstractResolver
    = ARLatestNightly
    | ARLatestLTS
    | ARLatestLTSMajor !Int
    | ARResolver !Resolver
    | ARGlobal
    deriving Show

readAbstractResolver :: ReadM AbstractResolver
readAbstractResolver = do
    s <- OA.readerAsk
    case s of
        "global" -> return ARGlobal
        "nightly" -> return ARLatestNightly
        "lts" -> return ARLatestLTS
        'l':'t':'s':'-':x | Right (x', "") <- decimal $ T.pack x ->
            return $ ARLatestLTSMajor x'
        _ ->
            case parseResolverText $ T.pack s of
                Left e -> OA.readerError $ show e
                Right x -> return $ ARResolver x
