{-# LANGUAGE                  
    GeneralizedNewtypeDeriving #-}

module Text.Transf (
        -- * Basic types
        Line,
        Lines,
        RelativePath,

        -- * The Context type
        Context,
        ContextT,
        runContext,
        runContextT,

        -- * Transformormations
        Transform,

        -- -- ** Creating new transformations
        transform,
        -- newTransform,

        -- ** Running transformations
        runTransform,
        -- runTransformIO,

        -- * Combinators
        -- ** Input/output
        readFile,
        writeFile,
        inform,
        -- ** Evaluation
        eval,
        evalWith,
        addPost,

        -- * Transformormations
        printT,
        evalT,   
        musicT,
        MusicOpts(..),
        musicT',
        haskellT,
        evalHaskellT,
        musicHaskellT,
        musicHaskellT',
        musicExtraT,
  ) where

import Prelude hiding (mapM, readFile, writeFile)

import Control.Applicative
import Control.Exception
import Control.Concurrent.Async
import Control.Monad
import Control.Monad.Writer hiding ((<>))
import Control.Monad.Error
import Control.Monad.Plus
import Numeric
import Data.Maybe
import Data.Default
import Data.Semigroup
import Data.Traversable
import Data.Typeable
import Data.Hashable
import System.IO (hPutStr, hPutStrLn, stderr)
import System.Process
import Language.Haskell.Interpreter hiding (eval)

import qualified Prelude
import qualified Data.List          as List
import qualified Data.Char          as Char
import qualified Data.Traversable   as Traversable
-- import qualified Music.Prelude.Basic as Music

-- |
-- A single line of text.
--
type Line = String

-- |
-- Multiple lines of text.
--
type Lines = String

-- | 
-- A relative file path.
--
type RelativePath = FilePath

-- | 
-- Action to be executed after main transf pass.
--
newtype Post m = Post [ContextT m ()]
    deriving (Monoid)

post :: ContextT m () -> Post m
post = Post . return

type PrimContextT m = ErrorT String (WriterT (Post m) m)

newtype ContextT m a = ContextT { runContextT_ :: PrimContextT m a }
    deriving ( Functor, Monad, MonadIO, MonadPlus, Applicative, 
      Alternative, MonadError String, MonadWriter (Post m) )

-- | 
-- The 'Context' monad defines the context of a transformation.
--
-- The main purpose of this type is to restrict the the number
-- of functions you can pass to 'transform'. 
--
type Context = ContextT IO

-- |
-- Run a computation in the 'Context' monad.
--
runContext :: Context a -> IO (Either String a)
runContext x = do
    (r, Post posts) <- runC x
    parallel_ (fmap ignoreErrorsAndPost posts)
    return r
    where
        runC = runWriterT . runErrorT . runContextT_

runContextT :: Monad m => ContextT m a -> m (Either String a)
runContextT = runContextT' True

runContextT' :: Monad m => Bool -> ContextT m a -> m (Either String a)
runContextT' recur x = do
    (r, Post posts) <- runC x
    if recur then runContextT' False (sequence_ posts) else return (return ())
    return r
    where
        runC = runWriterT . runErrorT . runContextT_

ignoreErrorsAndPost :: ContextT IO a -> IO ()
ignoreErrorsAndPost x = (runWriterT . runErrorT . runContextT_) x >> return ()





-- |
-- A transformation.
-- 
data Transform
    = CompTrans {
        decomp    :: [Transform]
    }
    | SingTrans {
        delimiters :: (Line -> Bool, Line -> Bool),
        function   :: Lines -> Context Lines
    }

doTrans (SingTrans _ f) = f

instance Semigroup Transform where
    a <> b = CompTrans [a,b]
instance Monoid Transform where
    mempty  = CompTrans []
    mappend = (<>)

-- | Create a new transformation. For example:
--
-- > newTransform start stop change
--
-- This creates a new transformation that searches its input for consecutive
-- sequences of lines delimited by lines accepted by the @start@ and @stop@
-- functions, and applies the given change function to these chunks.
--
-- To create a suitable change function, use the combinators defined below.
--
newTransform :: (Line -> Bool) -> (Line -> Bool) -> (Lines -> Context Lines) -> Transform
newTransform b e = SingTrans (b, e)

namedFence :: String -> String -> Bool
namedFence name = namedFenceWithPrefix "```" name `oneOf` namedFenceWithPrefix "~~~" name

namedFenceWithPrefix :: String -> String -> String -> Bool
namedFenceWithPrefix prefix name = (== (prefix ++ name)) . trimEnd

-- | Create a new transformation.
--
--   This transformation processes everything in between lines containing
--   a fence such as
--
--   > ~~~name
--   > ~~~
--
--   or
--
--   > ```name
--   > ```
--
--   where @name@ is the name of the transformation.
--
--   To create a suitable change function, use the combinators defined below.
--
transform :: String -> (Lines -> Context Lines) -> Transform
transform name = newTransform (namedFence name) (namedFence "")

-- | 
-- Run a transformation with the given error handler and input.
--
runTransformIO :: Transform -> (String -> IO String) -> String -> IO String
runTransformIO t handler input = do
    res <- runContext $ runTransform t input
    case res of
        Left e  -> handler e
        Right a -> return a

-- | 
-- Run a transformation in the 'Context' monad.
--
runTransform :: Transform -> String -> Context String
runTransform = go
    where
        go (CompTrans [])     as = return as
        go (CompTrans (t:ts)) as = do
            bs <- go t as
            go (CompTrans ts) bs
        go (SingTrans (start,stop) f) as = do
            let bs = (sections start stop . lines) as                   :: [([Line], Maybe [Line])]
            let cs = fmap (first unlines . second (fmap unlines)) bs    :: [(String, Maybe String)]
            ds <- Traversable.mapM (secondM (Traversable.mapM f)) cs    :: Context [(String, Maybe String)]
            return $ concatMap (\(a, b) -> a ++ fromMaybe [] b ++ "\n") ds

----------------------------------------------------------------------------------------------------

-- |
-- Read a file.
--
readFile :: RelativePath -> Context String
readFile path = do
    input <- liftIO $ try $ Prelude.readFile path
    case input of
        Left e  -> throwError $ "readFile: " ++ show (e::SomeException)
        Right a -> return a

-- appendFile   :: RelativePath -> String -> Context ()

-- |
-- Write to a file.
--
writeFile :: RelativePath -> String -> Context ()
writeFile path str = liftIO $ Prelude.writeFile path str

-- |
-- Evaluate a Haskell expression.
--
eval :: Typeable a => String -> Context a
eval = evalWith["Prelude", "Music.Prelude.Basic"] 
    -- FIXME hardcoded
    -- For some reason, Pitch needs to be in scope (type synonym exported from Music.Prelude.Basic)

-- | Evaluate a Haskell expression with the given modules in scope.
--   Note that "Prelude" is /not/ implicitly imported.
--
--
--   All requested modules must be present on the system or the computation
--   will fail. Also, the string must be a valid Haskell expression using
--   constructs which in scope after loading the given modules.
--
--   Errors can be caught using 'catchError'.
--
evalWith :: Typeable a => [String] -> String -> Context a
evalWith imps str = do
    res <- liftIO $ runInterpreter $ do
        set [languageExtensions := [OverloadedStrings, NoMonomorphismRestriction]]
        setImportsimps
        interpret str infer
    case res of
        Left e -> throwError $ "Could not evaluate: " ++ str ++ "\n" ++ showIE e
        Right a -> return a
    where
        showIE (WontCompile xs)  = "    " ++ List.intercalate "\n    " (fmap errMsg xs)
        showIE (UnknownError x)  = x
        showIE (NotAllowed x)    = x
        showIE (GhcException x)  = x

-- |
-- Write to the standard error stream.
--
inform :: String -> Context ()
inform m = liftIO $ hPutStr stderr $ m ++ "\n"

-- |
-- Register an action to be run after text processing has finished.
-- This can be used to optimize tasks such as external file generations.
--
-- Note that addPost does not work trasitively, i.e. post actions of
-- post actions are thrown away.
-- 
addPost :: Context () -> Context ()
addPost = tell . post


----------------------------------------------------------------------------------------------------

-- |
-- This named transformation posts its input to the standard error stream
-- and returns nothing.
--
printT :: Transform
printT = transform "print" $ \input -> inform input >> return ""

-- |
-- This named transformation evaluates its input as a Haskell expression of
-- type 'String' and returns the value of the expression.
--
-- For example the input
--
-- > ~~~haskell
-- > "The number is " ++ show $ 3 + 2
-- > ~~~
--
-- Will be transformed into
--
-- > The number is 6
--
evalT :: Transform
-- evalT = transform "eval" $ \input -> evalWith ["Prelude"] input
evalT = transform "eval" $ \input -> do
  (exit, out, err) <- liftIO $ readProcessWithExitCode "runhaskell2" [] input
  inform err
  return out

-- TODO move to separate module and/or package

data MusicOpts = MusicOpts {
        format     :: String,
        resolution :: Int,
        resize     :: Int,
        prelude    :: String
    }
instance Default MusicOpts where def = MusicOpts {
        format     = "png",
        resolution = 200,
        resize     = 45,
        prelude     = "basic"
    }

-- |
-- This named transformation evaluates its input as a music expression.
--
-- The music is rendered as an @.ly@ file and a @.mid@ fiel, then @lilypond@ and @convert@
-- is run to render a @.png@ file. A markdown image tag and a HTML play and stop button
-- is returned.
--
-- The expression must return a value of type @Score Note@. The "Music.Prelude.Basic"
-- module is implicitly imported.
--
musicT :: Transform
musicT = musicT' def

musicT' :: MusicOpts -> Transform
musicT' opts = transform "music" $ \input -> do
    let prel = prelude opts
    let name = showHex (abs $ hash input) ""

    {-
    music <- eval input :: Context (Music.Score Music.BasicNote)

    liftIO $ let handler ex = hPutStrLn stderr $ "transf (music+ly): " ++ show (ex::SomeException) ++ "\n" ++ show input
        in handler `handle` (Music.writeLilypond (name++".ly") music)

    liftIO $ Music.writeMidi (name++".mid") music
    -- liftIO $ void $ readProcess "timidity" ["-Ow", name++".mid"] ""
-}

    -- Use music2... wrappers rather than hint
    -- Note that the use of readProcess will propagate error messages from stderr
    -- (including both parse and type errors).

    currentFile <- liftIO $ tryMaybe $ Prelude.readFile (name++".music")
    unless (currentFile == Just input) $ do
      writeFile (name++".music") input
      liftIO $ void $ readProcess "music2ly"   ["--prelude", prel, "-o", name++".ly",  name++".music"] ""
      liftIO $ void $ readProcess "music2midi" ["--prelude", prel, "-o", name++".mid", name++".music"] ""

      let makeLy = do
          (exit, out, err) <- readProcessWithExitCode "lilypond" [
              "-f", format opts, 
              "-dresolution=" ++ show (resolution opts) ++ "", name++".ly"
              ] mempty
          hPutStr stderr out
          hPutStr stderr err
          return ()
    
      let makePng = when (format opts == "png") $ void $ system $ 
              "convert -transparent white -resize " 
                  ++ show (resize opts) ++"% "
                  ++ name ++".png "
                  ++ name ++ "x.png"

      addPost (liftIO $makeLy >> makePng)

    -- let playText = ""

    -- Play generated MIDI file
    let playText = "<div class='haskell-music-listen'><a href='"++name++".mid'>[listen]</a></div>"
    
    -- Play generated WAV file
    -- let playText = "<div class='haskell-music-listen'><a href='"++name++".wav'>[listen]</a></div>"

    -- Use Web MIDI player
    -- let playText = "<div>" ++
    --                "  <a href=\"javascript:playFile('"++name++".mid')\">[play]</a>\n" ++
    --                "  <a href=\"javascript:stopPlaying()\">[stop]</a>\n" ++
    --                "</div>\n"
                                                   
    let ending = if format opts == "png" then "x" else ""
    return $ playText ++ "\n\n" ++ "![](" ++ name ++ ending ++ "." ++ format opts ++ ")"
    --  -resize 30%

-- |
-- This named transformation includes stuff needed for music playback.
--
-- It should be used exactly once in the document.
--
musicExtraT :: Transform
musicExtraT = transform "music-extra" $ \_ -> return txt
    where
        txt = "<script src=\"js/jasmid/stream.js\"></script>\n" ++
              "<script src=\"js/jasmid/midifile.js\"></script>\n" ++
              "<script src=\"js/jasmid/replayer.js\"></script>\n" ++
              "<script src=\"js/midi.js\"></script>\n" ++
              "<script src=\"js/Base64.js\" type=\"text/javascript\"></script>\n" ++
              "<script src=\"js/base64binary.js\" type=\"text/javascript\"></script>\n" ++
              "<script src=\"js/main.js\" type=\"text/javascript\"></script>\n"

{-
    <script src="js/jasmid/stream.js"></script>
    <script src="js/jasmid/midifile.js"></script>
    <script src="js/jasmid/replayer.js"></script>
    <script src="js/midi.js"></script>
    <script src="js/Base64.js" type="text/javascript"></script>
    <script src="js/base64binary.js" type="text/javascript"></script>
    <script src="js/main.js" type="text/javascript"></script>
-}

    
-- |
-- This named transformation passes everything through and retains the source.
-- 
haskellT :: Transform
haskellT = transform "haskell" $ \input ->
    return $ "```haskell\n" ++ input ++ "\n```"

-- |
-- This named transformation runs the 'music' transformation and retains the source.
-- 
musicHaskellT :: Transform
musicHaskellT = musicHaskellT' def

musicHaskellT' :: MusicOpts -> Transform
musicHaskellT' opts = transform "music+haskell" $ \input -> do
    let begin    = "<div class='haskell-music'>"
    let end      = "</div>"
    musicRes   <- doTrans (musicT' opts) input
    haskellRes <- doTrans haskellT input
    return $ begin ++ "\n\n" ++ musicRes ++ "\n\n" ++ haskellRes ++ "\n\n" ++ end


evalHaskellT :: Transform
evalHaskellT = transform "eval+haskell" $ \input -> do
    evalRes    <- doTrans evalT input
    haskellRes <- doTrans haskellT input
    return $ "\n\n" ++ evalRes ++ "\n\n" ++ haskellRes ++ "\n\n"


----------------------------------------------------------------------------------------------------

-- | Separate the sections delimited by the separators from their context. Returns
--      [(outside1, inside1), (outside2, inside2)...]
--
sections :: (a -> Bool) -> (a -> Bool) -> [a] -> [([a], Maybe [a])]
sections start stop as = case (bs,cs) of
    ([], [])  -> []
    (bs, [])  -> [(bs, Nothing)]
    (bs, [c]) -> [(bs, Nothing)]
    (bs, cs)  -> (bs, Just $ tail cs) : sections start stop (drop skip as)
    where
        (bs,cs) = sections1 start stop as
        skip    = length bs + length cs + 1

sections1 :: (a -> Bool) -> (a -> Bool) -> [a] -> ([a],[a])
sections1 start stop as =
    (takeWhile (not . start) as, takeWhile (not . stop) $ dropWhile (not . start) as)


first  f (a, b) = (f a, b)
second f (a, b) = (a, f b)

trimEnd :: String -> String
trimEnd = List.dropWhileEnd Char.isSpace

secondM :: Monad m => (a -> m b) -> (c, a) -> m (c, b)
secondM f (a, b) = do
    b' <-  f b
    return (a, b')

oneOf :: (a -> Bool) -> (a -> Bool) -> a -> Bool
oneOf p q x = p x ||q x

parallel_ :: [IO ()] -> IO ()
parallel_ = foldb concurrently_ (return ())

concurrently_ :: IO a -> IO b -> IO ()
concurrently_ = concurrentlyWith (\x y -> ())

concurrentlyWith :: (a -> b -> c) -> IO a -> IO b -> IO c
concurrentlyWith f x y = uncurry f <$> x `concurrently` y

foldb :: (a -> a -> a) -> a -> [a] -> a
foldb f z []  = z  
foldb f z [x] = x 
foldb f z xs = let (as,bs) = split xs
    in foldb f z as `f` foldb f z bs
    where
        split xs = (take n xs, drop n xs) where n = length xs `div` 2


tryMaybe :: IO a -> IO (Maybe a)
tryMaybe action = do
  r <- try action
  return $case r of
    Left e  -> let e' = (e::SomeException) in Nothing
    Right x -> Just x