{-# LANGUAGE CPP                        #-}
{-# LANGUAGE DeriveDataTypeable         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}

module Turtle.Line
  ( Line
  , lineToText
  , textToLines
  , linesToText
  , textToLine
  , unsafeTextToLine
  , NewlineForbidden(..)
  ) where

import Data.Text (Text)
import qualified Data.Text as Text
#if __GLASGOW_HASKELL__ >= 708
import Data.Coerce
#endif
import Data.List.NonEmpty (NonEmpty(..))
import Data.String
#if __GLASGOW_HASKELL__ >= 710
#else
import Data.Monoid
#endif
import Data.Maybe
import Data.Typeable
import Control.Exception

import qualified Data.List.NonEmpty

-- | The `NewlineForbidden` exception is thrown when you construct a `Line`
-- using an overloaded string literal or by calling `fromString` explicitly
-- and the supplied string contains newlines. This is a programming error to
-- do so: if you aren't sure that the input string is newline-free, do not
-- rely on the @`IsString` `Line`@ instance.
--
-- When debugging, it might be useful to look for implicit invocations of
-- `fromString` for `Line`:
--
-- > >>> sh (do { line <- "Hello\nWorld"; echo line })
-- > *** Exception: NewlineForbidden
--
-- In the above example, `echo` expects its argument to be a `Line`, thus
-- @line :: `Line`@. Since we bind @line@ in `Shell`, the string literal
-- @\"Hello\\nWorld\"@ has type @`Shell` `Line`@. The
-- @`IsString` (`Shell` `Line`)@ instance delegates the construction of a
-- `Line` to the @`IsString` `Line`@ instance, where the exception is thrown.
--
-- To fix the problem, use `textToLines`:
--
-- > >>> sh (do { line <- select (textToLines "Hello\nWorld"); echo line })
-- > Hello
-- > World
data NewlineForbidden = NewlineForbidden
  deriving (Show, Typeable)

instance Exception NewlineForbidden

-- | A line of text (does not contain newlines).
newtype Line = Line Text
  deriving (Eq, Ord, Show, Monoid)

#if __GLASGOW_HASKELL__ >= 804
instance Semigroup Line where
  (<>) = mappend
#endif

instance IsString Line where
  fromString = fromMaybe (throw NewlineForbidden) . textToLine . fromString

-- | Convert a line to a text value.
lineToText :: Line -> Text
lineToText (Line t) = t

-- | Split text into lines. The inverse of `linesToText`.
textToLines :: Text -> NonEmpty Line
textToLines =
#if __GLASGOW_HASKELL__ >= 708
  Data.List.NonEmpty.fromList . coerce (Text.splitOn "\n")
#else
  Data.List.NonEmpty.fromList . map unsafeTextToLine . Text.splitOn "\n"
#endif

-- | Merge lines into a single text value.
linesToText :: [Line] -> Text
linesToText =
#if __GLASGOW_HASKELL__ >= 708
  coerce Text.unlines
#else
  Text.unlines . map lineToText
#endif

-- | Try to convert a text value into a line.
-- Precondition (checked): the argument does not contain newlines.
textToLine :: Text -> Maybe Line
textToLine = fromSingleton . textToLines
  where
    fromSingleton (a :| []) = Just a
    fromSingleton  _        = Nothing

-- | Convert a text value into a line.
-- Precondition (unchecked): the argument does not contain newlines.
unsafeTextToLine :: Text -> Line
unsafeTextToLine = Line