content: {-# LANGUAGE ExistentialQuantification, OverloadedStrings, RankNTypes #-}
  -- | Applying the user expression as directed by the HawkRuntime.
  --   The API may change at any time.
  module System.Console.Hawk.Runtime
    ( SomeRows(..)
    , processTable
    ) where
  
  import Control.Exception
  import Data.ByteString.Lazy.Char8 as B
  import Data.ByteString.Lazy.Search as Search
  import GHC.IO.Exception
  import System.IO
  
  import System.Console.Hawk.Args.Spec
  import System.Console.Hawk.Representable
  import System.Console.Hawk.Runtime.Base
  
  
  data SomeRows = forall a. Rows a => SomeRows a
  
  processTable :: HawkRuntime -> ([[B.ByteString]] -> SomeRows) -> HawkIO ()
  processTable runtime f = HawkIO $ do
      xss <- getTable (inputSpec runtime)
      case f xss of
        SomeRows x -> outputRows (outputSpec runtime) x
  
  
  getTable :: InputSpec -> IO [[B.ByteString]]
  getTable spec = splitIntoTable' <$> getInputString'
    where
      splitIntoTable' = splitIntoTable (inputFormat spec)
      getInputString' = getInputString (inputSource spec)
  
  getInputString :: InputSource -> IO B.ByteString
  getInputString NoInput = return B.empty
  getInputString UseStdin = B.getContents
  getInputString (InputFile f) = B.readFile f
  
  -- [[contents]]
  -- or
  -- [[record0], [record1], ...]
  -- or
  -- [[field0, field1, ...], [field0, field1, ...], ...]
  splitIntoTable :: InputFormat -> B.ByteString -> [[B.ByteString]]
  splitIntoTable RawStream = return . return
  splitIntoTable (Records sep format) = fmap splitIntoFields' . splitIntoRecords'
    where
      splitIntoFields' = splitIntoFields format
      splitIntoRecords' = splitAtSeparator sep
  
  -- [record]
  -- or
  -- [field0, field1, ...]
  splitIntoFields :: RecordFormat -> B.ByteString -> [B.ByteString]
  splitIntoFields RawRecord = return
  splitIntoFields (Fields sep) = splitAtSeparator sep
  
  splitAtSeparator :: Separator -> B.ByteString -> [B.ByteString]
  splitAtSeparator Whitespace = B.words
  splitAtSeparator (Delimiter "\n") = fmap dropWindowsNewline . B.lines
    where
      dropWindowsNewline :: B.ByteString -> B.ByteString
      dropWindowsNewline "" = ""
      dropWindowsNewline s
          | last_char == '\r' = s'
          | otherwise = s
        where
          last_char = B.last s
          n = B.length s
          s' = B.take (n - 1) s
  splitAtSeparator (Delimiter d) = Search.split d
  
  
  outputRows :: Rows a => OutputSpec -> a -> IO ()
  outputRows (OutputSpec _ spec) x = ignoringBrokenPipe $ do
      let s = join' (toRows x)
      B.putStr s
      hFlush stdout
    where
      join' = join (B.fromStrict $ recordDelimiter spec)
      toRows = repr (B.fromStrict $ fieldDelimiter spec)
  
      join :: B.ByteString -> [B.ByteString] -> B.ByteString
      join "\n" = B.unlines
      join sep  = B.intercalate sep
  
  -- Don't fret if stdout is closed early, that is the way of shell pipelines.
  ignoringBrokenPipe :: IO () -> IO ()
  ignoringBrokenPipe = handleJust isBrokenPipe $ \_ ->
      -- ignore the broken pipe
      return ()
    where
      isBrokenPipe e | ioe_type e == ResourceVanished = Just e
      isBrokenPipe _                                  = Nothing
  