{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-}

{-|
The classes in this library roughly encapulates things you can read or write
through a "handle", typically writing something "string like" to a file,
but the interface is very flexible.

Currently only 'String', 'ByteString' and 'Text' in their strict/lazy varieties
and builders are implemented.

However, the interface has the flexibility to write non-character data.
For example an implementation for a @[Int]@ could just write all the 'Int's in
binary to the file and read them back into a @[Int]@.

Also the interface can be defined for "handles" other than standard files.
Currently the only such instance that does not use 'Handle' is for 'hGetContents',
where one can call directly:

> hGetContents fileName

In this case, "handle" is a 'FilePath'.

The initial motivation of to avoid having to change all your function
calls if you change your string type, but it's actually more general than that now.

The point of this library is to be able to read and write raw data to the disk
(and perhaps other places) quickly without thinking too much about it.

It's not intended to be a replacement for say 'Binary'. It's intended to be lower
level. 'Binary' internally creates lazy bytestrings for the user to write to the
disk, this library is instead just about directly writing raw data.

Note that we currently only have classes for

Most of the functions should be fairly self explanatory having the same meaning
as in "Prelude" or "System.IO" but more general.
-}
module System.IO.StringLike.Impl (
  CanGetContents, CanGetContentsClass (hGetContents), getContents, readFile,
  CanGetLine, CanGetLineClass (hGetLine), getLine,
  CanPutStr, CanPutStrClass (hPutStr), putStr, writeFile, appendFile,
  interact,
  CanPutStrLn, CanPutStrLnClass (hPutStrLn), putStrLn,
  CanProxyT, CanProxyTo (canProxyTo), CanProxyFrom (canProxyFrom)
  ) where

import Prelude hiding (
  readFile,
  writeFile,
  appendFile,
  getContents,
  putStr,
  interact,
  getLine,
  putStrLn
  )

import qualified System.IO as IO
import System.IO (Handle, IOMode(ReadMode))
import Data.Semigroup ((<>))

import qualified Data.ByteString.Char8 as BSS
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.ByteString.Builder as BSB

import qualified Data.Text as TS
import qualified Data.Text.IO as TS

import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TL

import qualified Data.Text.Lazy.Builder as TB

--------------------------
-- CanGetContents Class --
--------------------------

{-|
Type synonym, for the common case of an ordinary 'Handle' in the 'IO'.
-}
type CanGetContents t = CanGetContentsClass IO Handle t

class Monad m => CanGetContentsClass m handleT t where
  {-| Generalised 'System.IO.hGetContents' -}
  hGetContents :: handleT -> m t
  default hGetContents :: (CanProxyFrom t, CanGetContentsClass m handleT (CanProxyT t)) => handleT -> m t
  hGetContents = (canProxyFrom <$>) . hGetContents

getContents :: CanGetContents t => IO t
getContents = hGetContents IO.stdin

readFile :: CanGetContents t => FilePath -> IO t
readFile filePath = IO.withFile filePath ReadMode hGetContents

instance (m ~ IO, CanGetContentsClass m Handle t) => CanGetContentsClass m FilePath t where
  hGetContents = readFile

----------------------
-- CanGetLine class --
----------------------

{-| Type synonym for 'CanGetLineClass'. See 'CanGetContents' for more details. -}
type CanGetLine t = CanGetLineClass IO Handle t

class CanGetContentsClass m handleT t => CanGetLineClass m handleT t where
  {-| Generalised 'System.IO.hGetLine' -}
  hGetLine :: handleT -> m t
  default hGetLine :: (CanProxyFrom t, CanGetLineClass m handleT (CanProxyT t)) => handleT -> m t
  hGetLine = (canProxyFrom <$>) . hGetLine

getLine :: CanGetLine t => IO t
getLine = hGetLine IO.stdin

------------------
-- CanPutStr class --
------------------

{-| Type synonym for 'CanPutStrClass'. See 'CanGetContents' for more details. -}
type CanPutStr t = CanPutStrClass IO Handle t

class Monad m => CanPutStrClass m handleT t where
  {-| Generalised 'System.IO.hPutStr' -}
  hPutStr :: handleT -> t -> m ()
  default hPutStr :: (CanProxyTo t, CanPutStrClass m handleT (CanProxyT t)) => handleT -> t -> m ()
  hPutStr handle str = hPutStr handle (canProxyTo str)

putStr :: CanPutStr t => t -> IO ()
putStr = hPutStr IO.stdout

writeFile :: CanPutStr t => FilePath -> t -> IO ()
writeFile f txt = IO.withFile f IO.WriteMode (\hdl -> hPutStr hdl txt)

appendFile :: CanPutStr t => FilePath -> t -> IO ()
appendFile f txt = IO.withFile f IO.AppendMode (\hdl -> hPutStr hdl txt)

interact :: (CanGetContents t, CanPutStr t) => (t -> t) -> IO ()
interact f = getContents >>= (putStr . f)

instance (m ~ IO, CanPutStrClass m Handle t) => CanPutStrClass m FilePath t where
  hPutStr = appendFile

-------------------------
-- CanPutStrLine class --
-------------------------

{-| Effective type synonym for 'CanPutStrLnClass'. See 'CanGetContents' for more details. -}
type CanPutStrLn t = CanPutStrLnClass IO Handle t

class CanPutStrClass m handleT t => CanPutStrLnClass m handleT t where
  {-| Generalised 'System.IO.hPutStrLn' -}
  hPutStrLn :: handleT -> t -> m ()
  default hPutStrLn :: (CanProxyTo t, CanPutStrLnClass m handleT (CanProxyT t)) => handleT -> t -> m ()
  hPutStrLn handle str = hPutStrLn handle (canProxyTo str)

putStrLn :: CanPutStrLn t => t -> IO ()
putStrLn s = hPutStrLn IO.stdout s

{-|
If you have a data structure where in many cases the simplest way to read or write
to it is just convert it to another you should define an instance of this class.

For example, the simple way to write @ByteString@ 'BSB.Builder's is just to
convert them to and from lazy 'ByteString's.

Defining classese 'CanProxyTo' and 'CanProxyFrom' will define default methods
for many of the other classes in this library.
These can still be overriden if desired but it will save you a lot
of boilerplate if you just which to convert your structure through some other.
-}
type family CanProxyT t

class CanProxyTo t where
  {-| How to convert to the type you will attempt to store -}
  canProxyTo :: t -> CanProxyT t

class CanProxyFrom t where
  {-| How to convert from the type you will attempt to store -}
  canProxyFrom :: CanProxyT t -> t

----------------------
-- String Instances --
----------------------

instance (m ~ IO) => CanGetContentsClass m Handle String where
  hGetContents = IO.hGetContents
instance (m ~ IO) => CanGetLineClass m Handle String where
  hGetLine = IO.hGetLine
instance (m ~ IO) => CanPutStrLnClass m Handle String where
  hPutStrLn = IO.hPutStrLn
instance (m ~ IO) => CanPutStrClass m Handle String where
  hPutStr = IO.hPutStr

---------------------------------
-- Strict Bytestring Instances --
---------------------------------

instance (m ~ IO) => CanGetContentsClass m Handle BSS.ByteString where
  hGetContents = BSS.hGetContents
instance (m ~ IO) => CanGetLineClass m Handle BSS.ByteString where
  hGetLine = BSS.hGetLine

instance (m ~ IO) => CanPutStrClass m Handle BSS.ByteString where
  hPutStr = BSS.hPutStr
instance (m ~ IO) => CanPutStrLnClass m Handle BSS.ByteString where
  hPutStrLn = BSS.hPutStrLn

-------------------------------
-- Lazy Bytestring Instances --
-------------------------------

instance (m ~ IO) => CanGetContentsClass m Handle BSL.ByteString where
  hGetContents = BSL.hGetContents
instance (m ~ IO) => CanGetLineClass m Handle BSL.ByteString where
  hGetLine = (BSL.fromStrict <$>) . BSS.hGetLine

instance (m ~ IO) => CanPutStrClass m Handle BSL.ByteString where
  hPutStr = BSL.hPutStr
instance (m ~ IO) => CanPutStrLnClass m Handle BSL.ByteString where
  hPutStrLn = BSL.hPutStrLn

----------------------------------
-- Bytestring Builder Instances --
----------------------------------

instance (Monad m, CanGetContentsClass m Handle BSL.ByteString) => CanGetContentsClass m Handle BSB.Builder where
instance CanGetLineClass m Handle BSL.ByteString => CanGetLineClass m Handle BSB.Builder

instance (m ~ IO) => CanPutStrClass m Handle BSB.Builder where
  hPutStr = BSB.hPutBuilder
instance CanPutStrClass m Handle BSB.Builder => CanPutStrLnClass m Handle BSB.Builder where
  hPutStrLn handle builder = hPutStr handle (builder <> BSB.char8 '\n')

type instance CanProxyT BSB.Builder = BSL.ByteString

instance CanProxyTo BSB.Builder where
  canProxyTo = BSB.toLazyByteString

instance CanProxyFrom BSB.Builder where
  canProxyFrom = BSB.lazyByteString

---------------------------
-- Strict Text Instances --
---------------------------

instance (m ~ IO) => CanGetContentsClass m Handle TS.Text where
  hGetContents = TS.hGetContents

instance (m ~ IO) => CanGetLineClass m Handle TS.Text where
  hGetLine = TS.hGetLine

instance (m ~ IO) => CanPutStrClass m Handle TS.Text where
  hPutStr = TS.hPutStr

instance (m ~ IO) => CanPutStrLnClass m Handle TS.Text where
  hPutStrLn = TS.hPutStrLn

-------------------------
-- Lazy Text Instances --
-------------------------

instance (m ~ IO) => CanGetContentsClass m Handle TL.Text where
  hGetContents = TL.hGetContents

instance (m ~ IO) => CanGetLineClass m Handle TL.Text where
  hGetLine = TL.hGetLine

instance (m ~ IO) => CanPutStrClass m Handle TL.Text where
  hPutStr = TL.hPutStr

instance (m ~ IO) => CanPutStrLnClass m Handle TL.Text where
  hPutStrLn = TL.hPutStrLn

----------------------------
-- Text Builder Instances --
----------------------------

instance (Monad m, CanGetContentsClass m Handle TL.Text) => CanGetContentsClass m Handle TB.Builder
instance CanGetLineClass m Handle TL.Text => CanGetLineClass m Handle TB.Builder

instance (Monad m, CanPutStrClass m Handle TL.Text) => CanPutStrClass m Handle TB.Builder
instance CanPutStrLnClass m Handle TL.Text => CanPutStrLnClass m Handle TB.Builder

type instance CanProxyT TB.Builder = TL.Text

instance CanProxyTo TB.Builder where
  canProxyTo = TB.toLazyText

instance CanProxyFrom TB.Builder where
  canProxyFrom = TB.fromLazyText