{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns        #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ViewPatterns          #-}

import           Control.Monad.Extra            (ifM)
import qualified Data.ByteString                as BS
import           Data.List                      (sort)
import           Data.List.Extra                (nubOn)
import           Data.List.NonEmpty             (NonEmpty, nonEmpty)
import qualified Data.List.NonEmpty             as NEL
import qualified Data.Map.Strict                as M
import           Data.Maybe                     (fromMaybe, mapMaybe)
import           Data.Semigroup.Foldable        (fold1)
import           Data.Text                      (Text)
import qualified Data.Text                      as T
import           Data.Text.Encoding             (encodeUtf8)
import           Distribution.Pretty            (prettyShow)
import           Distribution.Types.PackageId   (PackageIdentifier (..))
import           Distribution.Types.PackageName (PackageName, mkPackageName,
                                                 unPackageName)
import           OpenSSL                        (withOpenSSL)
import qualified Options.Applicative            as Opts
import           Stackage
import           System.Directory               (doesDirectoryExist,
                                                 listDirectory)
import           System.FilePath                (addTrailingPathSeparator,
                                                 takeBaseName, takeDirectory,
                                                 takeExtension, (</>))

main :: IO ()
main = withOpenSSL $ do
  Options{input} <- Opts.execParser $
                   Opts.info (Opts.helper <*> optionsParser) Opts.fullDesc
  text <- BS.readFile input
  stack <- readStack text
  let
    dir = (takeDirectory input)
  resolvers <- unroll dir stack
  let
    resolver = fold1 resolvers
    project = genProject stack resolver
    dirs = NEL.toList $ (dir </>) <$> localDirs project
  cabals <- concat <$> traverse (globExt ".cabal") dirs
  let
    -- assumes that .cabal files are named correctly, otherwise we need
    -- PackageDescription-Parsec.html#v:readGenericPackageDescription
    ignore = (mkPackageName . takeBaseName) <$> cabals
    freeze = genFreeze resolver ignore
  BS.writeFile (dir </> "cabal.project") (encodeUtf8 $ printProject project)
  BS.writeFile (dir </> "cabal.project.freeze") (encodeUtf8 $ printFreeze freeze)

globExt :: String -> FilePath -> IO [FilePath]
globExt ext path = do
  files <- ifM (doesDirectoryExist path) (listDirectory path) (pure [])
  pure $ filter ((ext ==) . takeExtension) files

printProject :: Project -> Text
printProject (Project (Ghc ghc) pkgs srcs) =
  T.concat [ "-- Generated by stackage-to-hackage from stack.yaml\n\n"
         , "with-compiler: ", ghc, "\n\n"
         , "packages:\n    ", packages, "\n\n"
         , sources, "\n"
         , "allow-older: *\n"
         , "allow-newer: *\n"
         ]
  where
    packages = T.intercalate "\n  , " (T.pack . addTrailingPathSeparator <$>
                                     NEL.toList pkgs)
    sources = T.intercalate "\n" (source =<< srcs)
    source Git{repo, commit, subdirs} =
      let base = T.concat [ "source-repository-package\n    "
                        , "type: git\n    "
                        , "location: ", repo, "\n    "
                        , "tag: ", commit, "\n"]
      in if null subdirs
         then [base]
         else (\d -> T.concat [base, "    subdir: ", d, "\n"]) <$> subdirs

data Project = Project Ghc (NonEmpty FilePath) [Git] deriving (Show)

localDirs :: Project -> NonEmpty FilePath
localDirs (Project _ nefp _ ) = nefp

genProject :: Stack -> Resolver -> Project
genProject Stack{packages} Resolver{compiler, deps} = Project
  (fromMaybe (Ghc "ghc") compiler)
  (fromMaybe (pure ".") (nonEmpty $ mapMaybe pickLocal packages))
  (nubOn repo $ mapMaybe pickGit deps)
  where
    pickLocal (Local p)    = Just p
    pickLocal (Location _) = Nothing
    pickGit (Hackage _ )  = Nothing
    pickGit (SourceDep g) = Just g

printFreeze :: Freeze -> Text
printFreeze (Freeze deps (Flags flags)) =
  T.concat [ "constraints:\n    ", constraints, "\n"]
  where
    constraints = T.intercalate "\n  , " (constrait <$> sort deps)
    constrait pkg =
      let name = (T.pack . unPackageName . pkgName $ pkg)
          ver  = (T.pack . prettyShow . pkgVersion $ pkg)
          base = T.concat [name, " ==", ver]
      in case M.lookup name flags of
        Nothing      -> base
        Just entries -> T.concat [ name, " ", (custom entries)
                                 , "\n  , ", base]
    custom (M.toList -> lst) = T.intercalate " " $ (renderFlag <$> lst)
    renderFlag (name, True)  = "+" <> name
    renderFlag (name, False) = "-" <> name

data Freeze = Freeze [PackageIdentifier] Flags deriving (Show)

genFreeze :: Resolver -> [PackageName] -> Freeze
genFreeze Resolver{deps, flags} ignore =
  let pkgs = filter noSelfs $ unPkgId <$> mapMaybe pick deps
      uniqpkgs = nubOn pkgName pkgs
   in Freeze uniqpkgs flags
  where pick (Hackage p)   = Just p
        pick (SourceDep _) = Nothing
        noSelfs (pkgName -> n) = notElem n ignore

data Options = Options
  { input      :: FilePath
  }
optionsParser :: Opts.Parser Options
optionsParser = Options
  <$> file
  where
    file = Opts.strArgument
             (  Opts.metavar "FILENAME"
             <> Opts.help "Input stack.yaml")