{-# LANGUAGE UnicodeSyntax, LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables, ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards, TemplateHaskell #-}

module DMenu.Run where

import Control.Exception
import Control.Monad.State.Strict hiding (filterM)
import Control.Lens
import Data.Maybe
import System.Exit
import System.Process
import System.Directory
import Prelude hiding (filter)

import DMenu.Options

-- | A state monad transformer in which the command line options of @dmenu@ can
-- be configured.
type DMenuT = StateT Options

-- | The 'MonadIO' constraint additionally allows to spawn processes with
-- @System.Process@ in between.
type MonadDMenu m = (MonadIO m, MonadState Options m)

-- | When a spawned process fails, this type is used to represent the exit code
-- and @stderr@ output.
type ProcessError = (Int, String)

-- | Run a @StateT Options m a@ action using the command line options from the
-- config file or an empty set of options as initial state.
--
-- For example
--
-- > import qualified DMenu
-- >
-- > main :: IO ()
-- > main = DMenu.run $ do
-- >   DMenu.numLines .= 10
-- >   DMenu.prompt   .= "run"
-- >   liftIO . print =<< DMenu.selectM ["A","B","C"]
run :: MonadIO m => DMenuT m a  m a
run ma = evalStateT ma =<< readConfigOrDef =<< getDefConfigPath

getDefConfigPath :: MonadIO m => m FilePath
getDefConfigPath = (++"/.haskell-dmenu") <$> liftIO getHomeDirectory

-- | Run DMenu with the command line options from @m@ and a list of 'String's
-- from which the user should choose.
selectM
  :: MonadDMenu m
  => [String]
     -- ^ List from which the user should select.
    m (Either ProcessError String)
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
selectM entries = do
  cfg  get
  liftIO $ do
    (exitCode, sOut, sErr)  readCreateProcessWithExitCode
      (proc (_binaryPath cfg) (optionsToArgs cfg))
      (unlines entries)
    pure $ case exitCode of
      ExitSuccess  Right $ head $ lines sOut
      ExitFailure i  Left (i, sErr)

-- | Convenience function combining 'run' and 'selectM'.
--
-- The following example has the same behavior as the example for @run@:
--
-- > import qualified DMenu
-- >
-- > main :: IO ()
-- > main = print =<< DMenu.select setOptions ["A","B","C"]
-- >
-- > setOptions :: DMenu.MonadDMenu m => m ()
-- > setOptions = do
-- >   DMenu.numLines .= 10
-- >   DMenu.prompt   .= "run"
select
  :: MonadIO m
  => DMenuT m ()
     -- ^ @State Options@ action which changes the default command line
     -- options.
    [String]
     -- ^ List from which the user should select.
    m (Either ProcessError String)
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
select m0 entries = run $ m0 >> selectM entries

-- | Same as 'selectM', but allows the user to select from a list of arbitrary
-- elements, which have a 'String' representation.
selectWithM
  :: MonadDMenu m
  => (a  String)
     -- ^ How to display an @a@ in @dmenu@.
    [a]
     -- ^ List from which the user should select.
    m (Either ProcessError a)
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
selectWithM f xs = fmap (fromJust . flip lookup m) <$> selectM (map f xs)
  where m = [ (f x, x) | x  xs ]

-- | Same as 'select', but allows the user to select from a list of arbitrary
-- elements, which have a 'String' representation.
--
-- For example
--
-- > import qualified DMenu
-- >
-- > main :: IO ()
-- > main = print =<< DMenu.selectWith setOptions show [1..10::Int]
-- >
-- > setOptions :: DMenu.MonadDMenu m => m ()
-- > setOptions = do
-- >   DMenu.numLines .= 10
-- >   DMenu.prompt   .= "run"
selectWith
  :: MonadIO m
  => DMenuT m ()
     -- ^ @State Options@ action which changes the default command line
     -- options.
    (a  String)
     -- ^ How to display an @a@ in @dmenu@.
    [a]
     -- ^ List from which the user should select.
    m (Either ProcessError a)
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
selectWith m0 f xs = run $ m0 >> selectWithM f xs



-- | Like 'selectM' but uses the @dmenu2@ option @filterMode@, which
-- returns not only the selected item, but all items which fuzzy match the
-- input term.
filterM
  :: MonadDMenu m
  => [String]
     -- ^ List from which the user should filter.
    m (Either ProcessError [String])
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
filterM entries = do
  cfg  (dmenu2 . filterMode .~ True) <$> get
  liftIO $ do
    (exitCode, sOut, sErr)  readCreateProcessWithExitCode
      (proc (_binaryPath cfg) (optionsToArgs cfg))
      (unlines entries)
    pure $ case exitCode of
      ExitSuccess  Right $ lines sOut
      ExitFailure i  Left (i, sErr)

-- | Like 'select' but uses the @dmenu2@ option @filterMode@, which
-- returns not only the selected item, but all items which fuzzy match the
-- input term.
filter
  :: MonadIO m
  => DMenuT m ()
     -- ^ @State Options@ action which changes the default command line
     -- options.
    [String]
     -- ^ List from which the user should select.
    m (Either ProcessError [String])
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
filter m0 entries = run $ m0 >> filterM entries

-- | Like 'selectWithM' but uses the @dmenu2@ option @filterMode@, which
-- returns not only the selected item, but all items which fuzzy match the
-- input term.
filterWithM
  :: MonadDMenu m
  => (a  String)
     -- ^ How to display an @a@ in @dmenu@.
    [a]
     -- ^ List from which the user should select.
    m (Either ProcessError [a])
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
filterWithM f xs = fmap (fmap (fromJust . flip lookup m)) <$> filterM (map f xs)
  where m = [ (f x, x) | x  xs ]

-- | Like 'selectWith' but uses the @dmenu2@ option @filterMode@, which
-- returns not only the selected item, but all items which fuzzy match the
-- input term.
filterWith
  :: MonadIO m
  => DMenuT m ()
     -- ^ @State Options@ action which changes the default command line
     -- options.
    (a  String)
     -- ^ How to display an @a@ in @dmenu@.
    [a]
     -- ^ List from which the user should select.
    m (Either ProcessError [a])
     -- ^ The selection made by the user, or a 'ProcessError', if the user
     -- canceled.
filterWith m0 f xs = run $ m0 >> filterWithM f xs


splitFirstWord :: String  (String, String)
splitFirstWord = go "" where
  go s []                           = (s, [])
  go s (c:cs) | c `elem` [' ','\t'] = (s, dropWhile (`elem` [' ','\t']) cs)
              | otherwise           = go (s++[c]) cs

readFileMay :: MonadIO m => FilePath  m (Maybe String)
readFileMay path = liftIO $
  (Just <$> readFile path) `catch` (\(_ :: SomeException)  pure Nothing)

readConfigOrDef :: MonadIO m => FilePath  m Options
readConfigOrDef = fmap f . readFileMay where
  f = \case
    Nothing  defOptions
    Just content  parseOptions content