{-# LANGUAGE NamedFieldPuns  #-}
{-# LANGUAGE RecordWildCards #-}
module Hi
  (
    run
  , process
  ) where

import           Hi.Directory            (inDirectory)
import           Hi.FilePath             (rewritePath)
import qualified Hi.Git                  as Git
import           Hi.Template             (readTemplates)
import           Hi.Types

import           Control.Applicative
import           Control.Monad
import qualified Data.ByteString.Char8   as BS
import qualified Data.ByteString.Lazy    as LBS (toChunks)
import           Data.List               ((\\))
import           Data.Maybe              (fromMaybe)
import qualified Data.Text               as T (pack, unpack)
import           Data.Text.Encoding      (decodeUtf8)
import           Data.Text.Lazy.Encoding (encodeUtf8)
import           Data.Text.Template      (Context, substitute)
import           System.Directory        (createDirectoryIfMissing)
import           System.FilePath         (dropFileName, joinPath, normalise,
                                          splitPath)
import           System.Process          (system)

-- | Run 'hi'.
run :: Option -> IO ()
run option@(Option {templateSource}) = do
    putStrLn $ "Creating new project with " ++ sourceName templateSource
    writeFiles =<< showFileList =<< process option <$> readTemplates templateSource
    postProcess option
  where
    sourceName (FromRepo repository) = "git repository:" ++ Git.expandUrl repository

-- |Write given 'Files' to filesystem.
writeFiles :: Files -> IO ()
writeFiles = mapM_ write

write :: File -> IO ()
write f = let path = getFilePath f
              contents = getFileContents f
          in createDirectoryIfMissing True (dropFileName path) >>
               BS.writeFile path contents

-- | Show 'Files' to stdout.
showFileList :: Files -> IO Files
showFileList files = do
    mapM_ (showFile . normalise. getFilePath) files
    return files
  where
    showFile :: FilePath -> IO ()
    showFile path = putStrLn $ "    " ++ green "create" ++ "  " ++ path
    green :: String -> String
    green x = "\x1b[32m" ++ x ++ "\x1b[0m"

-- |Process given 'Files' and return result. it does
--
-- 1. Rewrite path
-- 2. Substitute arguments
-- 3. Drop regular files if template file with same name exists
process :: Option -> Files -> Files
process Option {..} = dropExtraRegularFiles . map go . dropFilesInRoot
  where
    go (TemplateFile path content) = TemplateFile (rewritePath' path) (substitute' content)
    go (RegularFile  path content) = RegularFile  (rewritePath' path) content
    rewritePath' path = joinPath $ directoryName : splitPath (rewritePath packageName moduleName path)
    substitute' text = BS.concat . LBS.toChunks . encodeUtf8 $
                        substitute (decodeUtf8 text) (context options)
    options          = [("packageName", packageName)
                       ,("moduleName", moduleName)
                       ,("author", author)
                       ,("email", email)
                       ,("year", year)
                       ]

-- | Return 'Context' obtained by given 'Options'. An identifier which has no
-- corresponding context will not be substituted.
context :: [(String, String)] -> Context
context opts x = let x' = T.unpack x in T.pack . (fromMaybe ("$" ++ x')) $ lookup x' opts

postProcess :: Option -> IO ()
postProcess Option {directoryName, afterCommands} = do
    void $ inDirectory directoryName $ forM_ afterCommands (void . system)

-- | Drop 'RegularFile's if there is a 'TemplateFile' which has same name
--
-- >>> dropExtraRegularFiles [TemplateFile "foo" (BS.pack "e"), RegularFile "foo" (BS.pack "e")]
-- [TemplateFile {getFilePath = "foo", getFileContents = "e"}]
--
-- >>> dropExtraRegularFiles [RegularFile "foo" (BS.pack "e")]
-- [RegularFile {getFilePath = "foo", getFileContents = "e"}]
--
dropExtraRegularFiles :: Files -> Files
dropExtraRegularFiles []     = []
dropExtraRegularFiles xs = go (map getFilePath xs) xs
  where
    go _ []       = []
    go paths (y@(RegularFile p _):ys)  = if p `elem` (paths \\ [p])
                                           then go paths ys
                                           else y : go paths ys
    go paths (y@(TemplateFile _ _):ys) = y : go paths ys

-- | Drop all files in root directory.
--
-- >>> dropFilesInRoot [RegularFile "package-name/README.md" (BS.pack "a"), RegularFile "foo" (BS.pack "b")]
-- [RegularFile {getFilePath = "package-name/README.md", getFileContents = "a"}]
--
dropFilesInRoot :: Files -> Files
dropFilesInRoot []     = []
dropFilesInRoot (x:xs) = go (splitPath $ getFilePath x)
  where
    go [] = dropFilesInRoot xs
    go (path:_) = if "package-name/" == path
                    then x : dropFilesInRoot xs
                    else dropFilesInRoot xs