{-# LANGUAGE Rank2Types #-}
module Main where

import           Control.Applicative
                 (Applicative (..), (<$>), Const (..))
import           Control.Monad                          (when)
import           Data.Foldable
                 (foldMap, for_, traverse_)
import           Data.List                              (isPrefixOf, isSuffixOf)
import           Data.Maybe                             (mapMaybe, listToMaybe)
import           Data.Monoid                            (Monoid (..), Sum (..))
import           Data.Traversable                       (traverse)
import           Distribution.Simple.Utils              (fromUTF8LBS, ignoreBOM)
import           System.Directory
                 (getAppUserDataDirectory)
import           System.Environment                     (getArgs)
import           System.Exit                            (exitFailure)
import           System.FilePath                        ((</>))

import           Distribution.Types.Dependency
import           Distribution.Types.UnqualComponentName
import           Distribution.PackageDescription

import qualified Codec.Archive.Tar                      as Tar
import qualified Data.ByteString                        as B
import qualified Data.ByteString.Char8                  as B8
import qualified Data.ByteString.Lazy                   as BSL
import qualified Data.Map                               as Map
import qualified Distribution.PackageDescription.Parse  as ReadP
import qualified Distribution.PackageDescription.Parsec as Parsec
import qualified Distribution.Parsec.Parser             as Parsec
import qualified Distribution.Parsec.Types.Common       as Parsec
import qualified Distribution.ParseUtils                as ReadP
import qualified Distribution.Compat.DList              as DList

#if __GLASGOW_HASKELL__ >= 708
import Data.Coerce
#else
import Unsafe.Coerce
#endif

#ifdef HAS_STRUCT_DIFF
import           DiffInstances ()
import           StructDiff
#endif

parseIndex :: Monoid a => (FilePath -> BSL.ByteString -> IO a) -> IO a
parseIndex action = do
    cabalDir  <- getAppUserDataDirectory "cabal"
    cfg       <- B.readFile (cabalDir </> "config")
    cfgFields <- either (fail . show) pure $ Parsec.readFields cfg
    let repos        = reposFromConfig cfgFields
        repoCache    = case lookupInConfig "remote-repo-cache" cfgFields of
            []        -> cabalDir </> "packages"  -- Default
            (rrc : _) -> rrc                      -- User-specified
        tarName repo = repoCache </> repo </> "01-index.tar"
    mconcat <$> traverse (parseIndex' action . tarName) repos


parseIndex' :: Monoid a => (FilePath -> BSL.ByteString -> IO a) -> FilePath -> IO a
parseIndex' action path = do
    putStrLn $ "Reading index from: " ++ path
    contents <- BSL.readFile path
    let entries = Tar.read contents
    Tar.foldEntries (\e m -> mappend <$> f e <*> m) (return mempty) (fail . show) entries

  where
    f entry = case Tar.entryContent entry of
        Tar.NormalFile contents _
            | ".cabal" `isSuffixOf` fpath -> action fpath contents
            | otherwise                   -> return mempty
        Tar.Directory -> return mempty
        _             -> putStrLn ("Unknown content in " ++ fpath) >> return mempty
     where
       fpath = Tar.entryPath entry

readFieldTest :: FilePath -> BSL.ByteString -> IO ()
readFieldTest fpath bsl = case Parsec.readFields $ BSL.toStrict bsl of
    Right _  -> return ()
    Left err -> putStrLn $ fpath ++ "\n" ++ show err

-- | Map with unionWith monoid
newtype M k v = M (Map.Map k v)
    deriving (Show)
instance (Ord k, Monoid v) => Monoid (M k v) where
    mempty = M Map.empty
    mappend (M a) (M b) = M (Map.unionWith mappend a b)

compareTest
    :: String  -- ^ prefix of first packages to start traversal
    -> FilePath -> BSL.ByteString -> IO (Sum Int, Sum Int, M Parsec.PWarnType (Sum Int))
compareTest pfx fpath bsl
    | any ($ fpath) problematicFiles = mempty
    | not $ pfx `isPrefixOf` fpath   = mempty
    | otherwise = do
    let str = ignoreBOM $ fromUTF8LBS bsl

    putStrLn $ "::: " ++ fpath
    (readp, readpWarnings)  <- case ReadP.parseGenericPackageDescription str of
        ReadP.ParseOk ws x    -> return (x, ws)
        ReadP.ParseFailed err -> print err >> exitFailure
    traverse_ (putStrLn . ReadP.showPWarning fpath) readpWarnings

    let (warnings, errors, parsec') = Parsec.runParseResult $ Parsec.parseGenericPackageDescription (BSL.toStrict bsl)
    traverse_ (putStrLn . Parsec.showPWarning fpath) warnings
    traverse_ (putStrLn . Parsec.showPError fpath) errors
    parsec <- maybe (print readp >> exitFailure) return parsec'

    -- Old parser is broken for many descriptions, and other free text fields
    let readp0  = readp
            & set (packageDescription_ .  description_) ""
            & set (packageDescription_ .  synopsis_)    ""
            & set (packageDescription_ .  maintainer_)  ""
    let parsec0  = parsec
            & set (packageDescription_ .  description_) ""
            & set (packageDescription_ .  synopsis_)    ""
            & set (packageDescription_ .  maintainer_)  ""

    -- hs-source-dirs ".", old parser broken
    -- See e.g. http://hackage.haskell.org/package/hledger-ui-0.27/hledger-ui.cabal executable
    let parsecHsSrcDirs = parsec0 & toListOf (buildInfos_ . hsSourceDirs_)
    let readpHsSrcDirs  = readp0  & toListOf (buildInfos_ . hsSourceDirs_)
    let filterDotDirs   = filter (/= ".")

    let parsec1 = if parsecHsSrcDirs /= readpHsSrcDirs && fmap filterDotDirs parsecHsSrcDirs == readpHsSrcDirs
        then parsec0 & over (buildInfos_ . hsSourceDirs_) filterDotDirs
        else parsec0

    -- Compare two parse results
    if readp0 == parsec1
        then return ()
        else do
#if HAS_STRUCT_DIFF
            prettyResultIO $ diff readp parsec
#else
            putStrLn "<<<<<<"
            print readp
            putStrLn "======"
            print parsec
            putStrLn ">>>>>>"
#endif
            exitFailure

    let readpWarnCount  = Sum (length readpWarnings)
    let parsecWarnCount = Sum (length warnings)

    when (readpWarnCount > parsecWarnCount) $ do
        putStrLn "There are more readpWarnings"
        exitFailure

    let parsecWarnMap   = foldMap (\(Parsec.PWarning t _ _) -> M $ Map.singleton t 1) warnings
    return (readpWarnCount, parsecWarnCount, parsecWarnMap)

parseReadpTest :: FilePath -> BSL.ByteString -> IO ()
parseReadpTest fpath bsl = when (not $ any ($ fpath) problematicFiles) $ do
    let str = fromUTF8LBS bsl
    case ReadP.parseGenericPackageDescription str of
        ReadP.ParseOk _ _     -> return ()
        ReadP.ParseFailed err -> print err >> exitFailure

parseParsecTest :: FilePath -> BSL.ByteString -> IO ()
parseParsecTest fpath bsl = when (not $ any ($ fpath) problematicFiles) $ do
    let bs = BSL.toStrict bsl
    let (_warnings, errors, parsec) = Parsec.runParseResult $ Parsec.parseGenericPackageDescription bs
    case parsec of
        Just _ -> return ()
        Nothing -> do
            traverse_ (putStrLn . Parsec.showPError fpath) errors
            exitFailure

problematicFiles :: [FilePath -> Bool]
problematicFiles =
    [
    -- Indent failure
      eq "control-monad-exception-mtl/0.10.3/control-monad-exception-mtl.cabal"
    -- Other modules <- no dash
    , eq "DSTM/0.1/DSTM.cabal"
    , eq "DSTM/0.1.1/DSTM.cabal"
    , eq "DSTM/0.1.2/DSTM.cabal"
    -- colon : after section header
    , eq "ds-kanren/0.2.0.0/ds-kanren.cabal"
    , eq "ds-kanren/0.2.0.1/ds-kanren.cabal"
    , eq "metric/0.1.4/metric.cabal"
    , eq "metric/0.2.0/metric.cabal"
    , eq "phasechange/0.1/phasechange.cabal"
    , eq "shelltestrunner/1.3/shelltestrunner.cabal"
    , eq "smartword/0.0.0.5/smartword.cabal"
    -- \DEL
    , eq "vacuum-opengl/0.0/vacuum-opengl.cabal"
    , eq "vacuum-opengl/0.0.1/vacuum-opengl.cabal"
    -- dashes in version, not even tag
    , isPrefixOf "free-theorems-webui/"
    -- {- comment -}
    , eq "ixset/1.0.4/ixset.cabal"
    -- comments in braces
    , isPrefixOf "hint/"
    ]
  where
    eq = (==)

main :: IO ()
main = do
    args <- getArgs
    case args of
        ["read-field"]   -> parseIndex readFieldTest
        ["parse-readp"]  -> parseIndex parseReadpTest
        ["parse-parsec"] -> parseIndex parseParsecTest
        [pfx]            -> defaultMain pfx
        _                -> defaultMain ""
  where
    defaultMain pfx = do
        (Sum readpCount, Sum parsecCount, M warn) <- parseIndex (compareTest pfx)
        putStrLn $ "readp warnings: " ++ show readpCount
        putStrLn $ "parsec count:   " ++ show parsecCount
        for_ (Map.toList warn) $ \(t, Sum c) ->
            putStrLn $ " - " ++ show t ++ " : " ++ show c

-------------------------------------------------------------------------------
-- Index shuffling
-------------------------------------------------------------------------------

-- TODO: Use 'Cabal' for this?
reposFromConfig :: [Parsec.Field ann] -> [String]
reposFromConfig fields = takeWhile (/= ':') <$> mapMaybe f fields
  where
    f (Parsec.Field (Parsec.Name _ name) fieldLines)
        | B8.unpack name == "remote-repo" =
            Just $ fieldLinesToString fieldLines
    f (Parsec.Section (Parsec.Name _ name) [Parsec.SecArgName _ secName] _fieldLines)
        | B8.unpack name == "repository" =
            Just $ B8.unpack secName
    f _ = Nothing

-- | Looks up the given key in the cabal configuration file
lookupInConfig :: String -> [Parsec.Field ann] -> [String]
lookupInConfig key = mapMaybe f
  where
    f (Parsec.Field (Parsec.Name _ name) fieldLines)
        | B8.unpack name == key =
            Just $ fieldLinesToString fieldLines
    f _ = Nothing

fieldLinesToString :: [Parsec.FieldLine ann] -> String
fieldLinesToString fieldLines =
    B8.unpack $ B.concat $ bsFromFieldLine <$> fieldLines
  where
    bsFromFieldLine (Parsec.FieldLine _ bs) = bs

-------------------------------------------------------------------------------
-- Distribution.Compat.Lens
-------------------------------------------------------------------------------

type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s
type Traversal' s a = forall f. Applicative f => (a -> f a) -> s -> f s

type Getting r s a = (a -> Const r a) -> s -> Const r s
type ASetter' s a = (a -> I a) -> s -> I s



-- | View the value pointed to by a 'Getting' or 'Lens' or the
-- result of folding over all the results of a 'Control.Lens.Fold.Fold' or
-- 'Control.Lens.Traversal.Traversal' that points at a monoidal values.
view :: s -> Getting a s a -> a
view s l = getConst (l Const s)

-- | Replace the target of a 'Lens'' or 'Traversal'' with a constant value.
set :: ASetter' s a -> a -> s -> s
set l x = over l (const x)

-- | Modify the target of a 'Lens'' or all the targets of a 'Traversal''
-- with a function.
over :: ASetter' s a -> (a -> a) -> s -> s
#if __GLASGOW_HASKELL__ >= 708
over l f = coerce . l (coerce . f)
#else
over l f = unsafeCoerce . l (unsafeCoerce . f)
#endif

-- | Build a 'Lens'' from a getter and a setter.
lens :: (s -> a) -> (s -> a -> s) -> Lens' s a
lens sa sbt afb s = sbt s <$> afb (sa s)

-- | Build an 'Getting' from an arbitrary Haskell function.
to :: (s -> a) -> Getting r s a
to f g a = Const $ getConst $ g (f a)

-- | Extract a list of the targets of a 'Lens'' or 'Traversal''.
toListOf :: Getting (DList.DList a) s a -> s -> [a]
toListOf l = DList.runDList . getConst . l (Const . DList.singleton)

-- | Retrieve the first entry of a 'Traversal'' or retrieve 'Just' the result
-- from a 'Getting' or 'Lens''.
firstOf :: Getting (DList.DList a) s a -> s -> Maybe a
firstOf l = listToMaybe . toListOf l

-- | '&' is a reverse application operator
(&) :: a -> (a -> b) -> b
(&) = flip ($)
{-# INLINE (&) #-}
infixl 1 &

-------------------------------------------------------------------------------
-- Distribution.Compat.BasicFunctors
-------------------------------------------------------------------------------

newtype I a = I a

unI :: I a -> a
unI (I x) = x

instance Functor I where
    fmap f (I x) = I (f x)

instance Applicative I where
    pure        = I
    I f <*> I x = I (f x)
    _ *> x      = x

_2 :: Lens' (a, b) b
_2 = lens snd $ \(a, _) b -> (a, b)

-------------------------------------------------------------------------------
-- Distribution.PackageDescription.Lens
-------------------------------------------------------------------------------

packageDescription_ :: Lens' GenericPackageDescription PackageDescription
packageDescription_ = lens packageDescription $ \s a -> s { packageDescription = a }

condLibrary_ :: Lens' GenericPackageDescription (Maybe (CondTree ConfVar [Dependency] Library))
condLibrary_ = lens condLibrary $ \s a -> s { condLibrary = a}

condExecutables_ :: Lens' GenericPackageDescription [(UnqualComponentName, CondTree ConfVar [Dependency] Executable)]
condExecutables_ = lens condExecutables $ \s a -> s { condExecutables = a }

condTreeData_ :: Lens' (CondTree v c a) a
condTreeData_ = lens condTreeData $ \s a -> s { condTreeData = a }

description_, synopsis_, maintainer_ :: Lens' PackageDescription String
description_ = lens description $ \s a -> s { description = a }
synopsis_    = lens synopsis    $ \s a -> s { synopsis    = a }
maintainer_  = lens maintainer  $ \s a -> s { maintainer  = a }

class HasBuildInfo a where
    buildInfo_ :: Lens' a BuildInfo

instance HasBuildInfo Library where
    buildInfo_ = lens libBuildInfo $ \s a -> s { libBuildInfo = a }

instance HasBuildInfo Executable where
    buildInfo_ = lens buildInfo $ \s a -> s { buildInfo = a }

-- | This forgets a lot of structure, but might be nice for some stuff
buildInfos_ :: Traversal' GenericPackageDescription BuildInfo
buildInfos_ f gpd = mkGpd
    <$> (traverse . traverse . buildInfo_) f (condLibrary gpd)
    <*> (traverse . _2 . traverse . buildInfo_) f (condExecutables gpd)
  where
      mkGpd lib exe = gpd
          { condLibrary     = lib
          , condExecutables = exe
          }

hsSourceDirs_ :: Lens' BuildInfo [FilePath]
hsSourceDirs_ = lens hsSourceDirs $ \s a -> s { hsSourceDirs = a }
