{-# language UndecidableInstances #-}
{-# language CPP #-}
{-# language ConstraintKinds #-}
{-# language DefaultSignatures #-}
{-# language TypeFamilies #-}
{-# language TypeOperators #-}
{-# language MultiWayIf #-}

module Nix.Render where

import           Nix.Prelude
import qualified Data.Set                      as Set
import           Nix.Utils.Fix1                 ( Fix1T
                                                , MonadFix1T
                                                )
import           Nix.Expr.Types                 ( NPos(..)
                                                , NSourcePos(..)
                                                )
import           Nix.Expr.Types.Annotated
import           Prettyprinter
import qualified System.Directory              as S
import qualified System.PosixCompat.Files      as S
import           Text.Megaparsec.Error
import           Text.Megaparsec.Pos
import qualified Data.Text                     as Text

class (MonadFail m, MonadIO m) => MonadFile m where
    readFile :: Path -> m Text
    default readFile :: (MonadTrans t, MonadIO m', MonadFile m', m ~ t m') => Path -> m Text
    readFile = liftIO . Nix.Prelude.readFile
    listDirectory :: Path -> m [Path]
    default listDirectory :: (MonadTrans t, MonadFile m', m ~ t m') => Path -> m [Path]
    listDirectory = lift . listDirectory
    getCurrentDirectory :: m Path
    default getCurrentDirectory :: (MonadTrans t, MonadFile m', m ~ t m') => m Path
    getCurrentDirectory = lift getCurrentDirectory
    canonicalizePath :: Path -> m Path
    default canonicalizePath :: (MonadTrans t, MonadFile m', m ~ t m') => Path -> m Path
    canonicalizePath = lift . canonicalizePath
    getHomeDirectory :: m Path
    default getHomeDirectory :: (MonadTrans t, MonadFile m', m ~ t m') => m Path
    getHomeDirectory = lift getHomeDirectory
    doesPathExist :: Path -> m Bool
    default doesPathExist :: (MonadTrans t, MonadFile m', m ~ t m') => Path -> m Bool
    doesPathExist = lift . doesPathExist
    doesFileExist :: Path -> m Bool
    default doesFileExist :: (MonadTrans t, MonadFile m', m ~ t m') => Path -> m Bool
    doesFileExist = lift . doesFileExist
    doesDirectoryExist :: Path -> m Bool
    default doesDirectoryExist :: (MonadTrans t, MonadFile m', m ~ t m') => Path -> m Bool
    doesDirectoryExist = lift . doesDirectoryExist
    getSymbolicLinkStatus :: Path -> m S.FileStatus
    default getSymbolicLinkStatus :: (MonadTrans t, MonadFile m', m ~ t m') => Path -> m S.FileStatus
    getSymbolicLinkStatus = lift . getSymbolicLinkStatus

instance MonadFile IO where
  readFile              = Nix.Prelude.readFile
  listDirectory         = coerce S.listDirectory
  getCurrentDirectory   = coerce S.getCurrentDirectory
  canonicalizePath      = coerce S.canonicalizePath
  getHomeDirectory      = coerce S.getHomeDirectory
  doesPathExist         = coerce S.doesPathExist
  doesFileExist         = coerce S.doesFileExist
  doesDirectoryExist    = coerce S.doesDirectoryExist
  getSymbolicLinkStatus = coerce S.getSymbolicLinkStatus


instance (MonadFix1T t m, MonadIO (Fix1T t m), MonadFail (Fix1T t m), MonadFile m) => MonadFile (Fix1T t m)

posAndMsg :: NSourcePos -> Doc a -> ParseError s Void
posAndMsg (NSourcePos _ (coerce -> lineNo) _) msg =
  FancyError
    (unPos lineNo)
    (Set.fromList $ one (ErrorFail (show msg) :: ErrorFancy Void))

renderLocation :: MonadFile m => SrcSpan -> Doc a -> m (Doc a)
renderLocation (SrcSpan (NSourcePos file (coerce -> begLine) (coerce -> begCol)) (NSourcePos file' (coerce -> endLine) (coerce -> endCol))) msg
  | file == file' && file == "<string>" && begLine == endLine =
    pure $ "In raw input string at position " <> pretty (unPos begCol)

  | file /= "<string>" && file == file' =
    bool
      (pure msg)
      (do
        txt <- sourceContext file begLine begCol endLine endCol msg
        pure $
          vsep
            [ "In file " <> errorContext file begLine begCol endLine endCol <> ":"
            , txt
            ]
      )
      =<< doesFileExist file
renderLocation (SrcSpan beg end) msg = fail $ "Don't know how to render range from " <> show beg <>" to " <> show end <>" for fail: " <> show msg

errorContext :: Path -> Pos -> Pos -> Pos -> Pos -> Doc a
errorContext (coerce @Path @FilePath -> path) bl bc _el _ec =
  pretty path <> ":" <> pretty (unPos bl) <> ":" <> pretty (unPos bc)

sourceContext
  :: MonadFile m => Path -> Pos -> Pos -> Pos -> Pos -> Doc a -> m (Doc a)
sourceContext path (unPos -> begLine) (unPos -> _begCol) (unPos -> endLine) (unPos -> _endCol) msg
  = do
    let beg' = max 1 $ begLine - 3
        end' =         endLine + 3
    ls <-
      fmap pretty
      .   take (end' - beg')
      .   drop (pred beg')
      .   lines
      <$> Nix.Render.readFile path
    let
      longest = Text.length $ show $ beg' + length ls - 1
      pad :: Int -> Text
      pad n =
        let
          ns :: Text
          ns = show n
          nsp = Text.replicate (longest - Text.length ns) " " <> ns
        in
          if
          | n == begLine && n == endLine -> "==> " <> nsp <> " |  "
          | n >= begLine && n <= endLine -> "  > " <> nsp <> " |  "
          | otherwise                    -> "    " <> nsp <> " |  "
      composeLine n l =
        one (pretty (pad n) <> l)
        <> whenTrue
            (one $
              pretty $
                Text.replicate (Text.length (pad n) - 3) " "
                <> "|"
                <> Text.replicate (_begCol + 1) " "
                <> Text.replicate (_endCol - _begCol) "^"
            )
            (begLine == endLine && n == endLine)
        -- XXX: Consider inserting the message here when it is small enough.
        -- ATM some messages are so huge that they take prevalence over the source listing.
        -- ++ [ indent (length $ pad n) msg | n == endLine ]

      ls' = fold $ zipWith composeLine [beg' ..] ls

    pure $ vsep $ ls' <> one (indent (Text.length $ pad begLine) msg)
