module Web.Herringbone.Internal.Types where

import Control.Monad.Reader
import Control.Applicative
import Data.Maybe
import Data.Map (Map)
import qualified Data.Map as M
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock
import Data.Time.Format
import System.Locale
import System.IO hiding (FilePath)
import qualified Data.ByteString as B
import qualified Filesystem.Path.CurrentOS as F
import qualified Filesystem as F
import Filesystem.Path.CurrentOS (FilePath)
import Prelude hiding (FilePath)

-- | A value describing an error that occurred while trying to produce an
-- 'Asset'.
data AssetError = AssetNotFound
                | AssetCompileError CompileError
                | AmbiguousSources [FilePath]
                deriving (Show, Eq)

-- | Data which is given to preprocessors on the off-chance that they need it
-- (eg, Fay)
data PPReader = PPReader
    { ppReaderHb          :: Herringbone
    -- ^ The Herringbone which was used to build the asset
    , ppReaderSourcePath  :: FilePath
    -- ^ The file path to the source file
    , ppReaderPPs         :: [PP]
    -- ^ Preprocessors being invoked. Currently this will only ever have zero
    -- or one elements; in the future, Herringbone may be able to run multiple
    -- preprocessors on a single file.
    }

-- | A monad in which preprocessor actions happen.
newtype PPM a = PPM { unPPM :: ReaderT PPReader IO a }
    deriving (Functor, Applicative, Monad, MonadIO, (MonadReader PPReader))

runPPM :: PPM a -> PPReader -> IO a
runPPM comp = runReaderT (unPPM comp)

-- | A preprocessor something which is run on the asset before it is served.
-- Preprocessors are run when a file matches its rule.  For example, if you
-- have a preprocessor which takes \"coffee\" files and emits \"js\" files,
-- there is a file named \"application.coffee\", and you request
-- \"application.js\", Herringbone will run the coffee preprocessor on
-- \"application.coffee\" and serve you the result.
data PP = PP
    { ppName     :: Text
    -- ^ Identifies a preprocessor. Mainly useful for debugging compile errors.
    , ppConsumes :: Text
    -- ^ Extension for files this preprocessor consumes.
    , ppProduces :: Text
    -- ^ Extension for files this preprocessor produces.
    , ppAction :: PPAction
    -- ^ Performs the compilation.
    }

instance Show PP where
    show (PP name consumes produces _) = concat
        [ "<PP ", T.unpack name,
            " (", T.unpack consumes, " -> ", T.unpack produces, ")>"
        ]

-- | A function which performs the compilation.
type PPAction =
    B.ByteString
    -- ^ Input file contents
    -> PPM (Either CompileError B.ByteString)
    -- ^ Output file contents, or a compile error.

-- | A string which should contain information about why an asset failed to
-- compile.
type CompileError = B.ByteString

-- | A collection of preprocessors. This can store many preprocessors which
-- produce files with the same extension, but may not store more than one
-- preprocessor which consumes files of a particular extension.
newtype PPs = PPs { unPPs :: Map Text PP }
    deriving (Show)

emptyPPs :: PPs
emptyPPs = PPs M.empty

-- | Given a file extension, find the preprocessor (if any) which consumes it.
lookupPP :: Text -> PPs -> Maybe PP
lookupPP ext pps = M.lookup ext (unPPs pps)

-- | Inserts a preprocessor into a PPs. If a preprocessor already exists with
-- the given extension, it is discarded.
insertPP :: PP -> PPs -> PPs
insertPP pp pps = PPs $ M.insert (ppConsumes pp) pp (unPPs pps)

-- | Turn a list of PPs into a proper 'PPs'.
fromList :: [PP] -> PPs
fromList = foldr insertPP emptyPPs

-- | A BuildSpec specifies how an asset should be built.
data BuildSpec = BuildSpec
    { bsSourcePath :: FilePath  -- ^ Source path (relative)
    , bsDestPath   :: FilePath  -- ^ Destination path (again, relative)
    , bsPP         :: (Maybe PP)  -- ^ Preprocessor to run (if any)
    }
    deriving (Show)

-- | A BuildMapping contains the information to build all of the assets
-- Herringbone is aware of.
type BuildMapping = [BuildSpec]

-- | The \'main\' datatype in this library.  All of the important functions
-- will take a 'Herringbone' as their first argument.
data Herringbone = Herringbone
    { herringboneSettings :: HerringboneSettings
    , herringboneStartTime :: UTCTime
    }

-- | Contains configuration.
data HerringboneSettings = HerringboneSettings
    { settingsSourceDir :: FilePath
    -- ^ The directory to take asset sources from.
    , settingsDestDir    :: FilePath
    -- ^ Where to copy assets to after they've been compiled.
    , settingsPPs        :: PPs
    -- ^ Preprocessors
    , settingsVerbose    :: Bool
    -- ^ Dump debugging data to stdout on every request.
    }
    deriving (Show)

type ConfigBuilder = HerringboneSettings -> HerringboneSettings

-- | The directory where Herringbone will look when searching for assets.
hbSourceDir :: Herringbone -> FilePath
hbSourceDir = settingsSourceDir . herringboneSettings

-- | The directory to place assets in after compilation.
hbDestDir :: Herringbone -> FilePath
hbDestDir = settingsDestDir . herringboneSettings

-- | The collection of preprocessors that will be used when preprocessing
-- assets.
hbPPs :: Herringbone -> PPs
hbPPs = settingsPPs . herringboneSettings

-- | True iff the 'Herringbone' has the verbose setting enabled.
hbVerbose :: Herringbone -> Bool
hbVerbose = settingsVerbose . herringboneSettings

-- | Log a message to stdout if hbVerbose is enabled.
verbosePut :: Herringbone -> String -> IO ()
verbosePut hb msg = when (hbVerbose hb) $ do
    putStrLn ("herringbone: " ++ msg)
    hFlush stdout

-- | All assets in Herringbone are referenced by their logical path. This is
-- the path to an asset, relative to the source directory.
newtype LogicalPath = LogicalPath { fromLogicalPath :: [Text] }
    deriving (Show, Eq)

-- | Create a LogicalPath from a list of path segments. For example,
-- @[\"data\", \"dogs.txt\"]@ would map to data/dogs.txt (relative to the
-- source directory).  This returns Nothing if the path would be unsafe (that
-- is, if it contains \"..\"), to prevent directory traversal attacks.
makeLogicalPath :: [Text] -> Maybe LogicalPath
makeLogicalPath xs = if safe xs then Just $ LogicalPath xs else Nothing
    where
        safe = all (not . (==) "..")

-- | Create a LogicalPath without checking any of the values.
unsafeMakeLogicalPath :: [Text] -> LogicalPath
unsafeMakeLogicalPath = LogicalPath

toFilePath :: LogicalPath -> FilePath
toFilePath = F.concat . map F.fromText . fromLogicalPath

-- | A preprocessed asset. Any function that returns this will already have
-- done the preprocessing (if necessary).
data Asset = Asset
    { assetSize         :: Integer
    -- ^ Size of the asset in bytes.
    , assetSourcePath   :: FilePath
    -- ^ Path to the asset's source file on disk.
    , assetFilePath     :: FilePath
    -- ^ Path to the preprocessed asset on disk. Note that assets which do not
    -- require preprocessing will still be copied to the destination directory.
    , assetModifiedTime :: UTCTime
    -- ^ Modification time of the asset's source file.
    }

assetContent :: Asset -> IO B.ByteString
assetContent = F.readFile . assetFilePath

instance Show Asset where
    show (Asset size sourcePath filePath modifiedTime) =
        "Asset { " ++
        "assetSize = " ++ show size ++ ", " ++
        "assetSourcePath = " ++ show sourcePath ++ ", " ++
        "assetFilePath = " ++ show filePath ++ ", " ++
        "assetModifiedTime = " ++ showTime modifiedTime ++ " }"

        where
        showTime = formatTime defaultTimeLocale (dateTimeFmt defaultTimeLocale)