{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | This module contains functions for file handling. The names of the
-- functions and data types are chosen in a way to suggest a qualified import.
--
-- @
-- __import qualified__ Require.File __as__ File
-- @
module Require.File where

import Relude
import qualified Data.Text.IO as TIO

-- | Wraps the name of a file as given by the user. Usually this corresponds to
-- the file's path.
newtype Name = Name Text

-- | Associates a file's contents with the path from which it was read.
data Input = Input
  { Input -> Name
inputName :: Name,
    Input -> Text
inputContent :: Text
  }

-- | A type-safe wrapper for line numbers.
newtype LineNumber = LineNumber Int
  deriving (Int -> LineNumber
LineNumber -> Int
LineNumber -> [LineNumber]
LineNumber -> LineNumber
LineNumber -> LineNumber -> [LineNumber]
LineNumber -> LineNumber -> LineNumber -> [LineNumber]
(LineNumber -> LineNumber)
-> (LineNumber -> LineNumber)
-> (Int -> LineNumber)
-> (LineNumber -> Int)
-> (LineNumber -> [LineNumber])
-> (LineNumber -> LineNumber -> [LineNumber])
-> (LineNumber -> LineNumber -> [LineNumber])
-> (LineNumber -> LineNumber -> LineNumber -> [LineNumber])
-> Enum LineNumber
forall a.
(a -> a)
-> (a -> a)
-> (Int -> a)
-> (a -> Int)
-> (a -> [a])
-> (a -> a -> [a])
-> (a -> a -> [a])
-> (a -> a -> a -> [a])
-> Enum a
enumFromThenTo :: LineNumber -> LineNumber -> LineNumber -> [LineNumber]
$cenumFromThenTo :: LineNumber -> LineNumber -> LineNumber -> [LineNumber]
enumFromTo :: LineNumber -> LineNumber -> [LineNumber]
$cenumFromTo :: LineNumber -> LineNumber -> [LineNumber]
enumFromThen :: LineNumber -> LineNumber -> [LineNumber]
$cenumFromThen :: LineNumber -> LineNumber -> [LineNumber]
enumFrom :: LineNumber -> [LineNumber]
$cenumFrom :: LineNumber -> [LineNumber]
fromEnum :: LineNumber -> Int
$cfromEnum :: LineNumber -> Int
toEnum :: Int -> LineNumber
$ctoEnum :: Int -> LineNumber
pred :: LineNumber -> LineNumber
$cpred :: LineNumber -> LineNumber
succ :: LineNumber -> LineNumber
$csucc :: LineNumber -> LineNumber
Enum)

-- | Identifies a specific line in a file.
data LineTag = LineTag !Name !LineNumber

-- | Returns the 'LineTag' referencing the first line in a given 'FileInput'.
--
-- Note that the tag's line number is 1-based, which fits well with how GHC
-- understands @{-\# LINE ... #-}@ pragmas.
initialLineTag :: Input -> LineTag
initialLineTag :: Input -> LineTag
initialLineTag Input
inp = Name -> LineNumber -> LineTag
LineTag (Input -> Name
inputName Input
inp) (Int -> LineNumber
LineNumber Int
1)

-- | Returns a line tag from the same file but referencing the next line.
advanceLineTag :: LineTag -> LineTag
advanceLineTag :: LineTag -> LineTag
advanceLineTag (LineTag Name
fn LineNumber
ln) = Name -> LineNumber -> LineTag
LineTag Name
fn (LineNumber -> LineNumber
forall a. Enum a => a -> a
succ LineNumber
ln)

-- | @read name@ reads the contents of the file identified by @name@.
read :: Name -> IO Input
read :: Name -> IO Input
read Name
f = Name -> Text -> Input
Input Name
f (Text -> Input) -> IO Text -> IO Input
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> IO Text
forall (m :: * -> *). MonadIO m => FilePath -> m Text
readFileText (Name -> FilePath
nameToPath Name
f)

-- | @write name lines@ writes all every line in @lines@ to the file identified
-- by @name@ and appends a newline.
writeLines :: Name -> [Text] -> IO ()
writeLines :: Name -> [Text] -> IO ()
writeLines Name
name [Text]
theLines = FilePath -> IOMode -> (Handle -> IO ()) -> IO ()
forall r. FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile (Name -> FilePath
nameToPath Name
name) IOMode
WriteMode ((Handle -> IO ()) -> IO ()) -> (Handle -> IO ()) -> IO ()
forall a b. (a -> b) -> a -> b
$ \Handle
h ->
  (Text -> IO ()) -> [Text] -> IO ()
forall (t :: * -> *) (f :: * -> *) a b.
(Foldable t, Applicative f) =>
(a -> f b) -> t a -> f ()
traverse_ (Handle -> Text -> IO ()
TIO.hPutStrLn Handle
h) [Text]
theLines

-- | Splits the input into lines and annotates each with a 'LineTag'.
inputLines :: Input -> [(LineTag, Text)]
inputLines :: Input -> [(LineTag, Text)]
inputLines Input
fi = [LineTag] -> [Text] -> [(LineTag, Text)]
forall a b. [a] -> [b] -> [(a, b)]
zip [LineTag]
lineTags [Text]
contents
  where
    lineTags :: [LineTag]
lineTags = (LineTag -> LineTag) -> LineTag -> [LineTag]
forall a. (a -> a) -> a -> [a]
iterate LineTag -> LineTag
advanceLineTag (Input -> LineTag
initialLineTag Input
fi)
    contents :: [Text]
contents = Text -> [Text]
forall t. IsText t "lines" => t -> [t]
lines (Input -> Text
inputContent Input
fi)

-- | Returns the 'FilePath' corresponding a given 'Name'.
nameToPath :: Name -> FilePath
nameToPath :: Name -> FilePath
nameToPath (Name Text
fp) = Text -> FilePath
forall a. ToString a => a -> FilePath
toString Text
fp