{-
-- EPITECH PROJECT, 2026
-- Glados
-- File description:
-- Io Instructions
-}

{-|
Module      : VM.Instruction.Io
Description : System Input/Output primitives.
Stability   : experimental

This module implements low-level file system interactions (Open, Read, Write, Close).
It acts as a bridge between the VM's stack-based memory and the Host OS.
-}
module VM.Instruction.Io
    ( instOpen
    , instRead
    , instWrite
    , instClose
    , instInput
    ) where

import Control.Exception (try, IOException)
import Control.Monad.State.Strict (get, put, liftIO)
import qualified Data.Map.Strict as Map
import qualified Data.Vector as V
import qualified Data.Text as T
import System.IO

import VM.VMState (VirtualMachine, VMState(..))
import VM.VMValue (VMValue(..), valueToInt, valueToString, stringToValue)
import VM.VMStack (stackPop, stackPush)
import Common.Type.Integer (IntValue(..))

-- | Wrapper to attempt opening a file safely.
--
-- @arg path: The target file path string.
-- @arg mode: The Handle mode (Read/Write/Append).
--
-- @details
--   Catches any IOException using 'try' to prevent VM crash.
--   Returns an Either type to be handled by the caller.
--
tryOpen :: FilePath -> IOMode -> VirtualMachine (Either IOException Handle)
tryOpen :: FilePath -> IOMode -> VirtualMachine (Either IOException Handle)
tryOpen FilePath
path IOMode
mode = IO (Either IOException Handle)
-> VirtualMachine (Either IOException Handle)
forall a. IO a -> StateT VMState IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either IOException Handle)
 -> VirtualMachine (Either IOException Handle))
-> IO (Either IOException Handle)
-> VirtualMachine (Either IOException Handle)
forall a b. (a -> b) -> a -> b
$ IO Handle -> IO (Either IOException Handle)
forall e a. Exception e => IO a -> IO (Either e a)
try (FilePath -> IOMode -> IO Handle
openFile FilePath
path IOMode
mode)

-- | Converts an integer to a standard IOMode.
--
-- @arg i: The integer representation (0, 1, 2).
--
-- @details
--   Mapping:
--   1 -> WriteMode
--   2 -> AppendMode
--   Default -> ReadMode
--
getIoMode :: Int -> IOMode
getIoMode :: Int -> IOMode
getIoMode Int
1 = IOMode
WriteMode
getIoMode Int
2 = IOMode
AppendMode
getIoMode Int
_ = IOMode
ReadMode

-- | Registers a new Handle in the VM state.
--
-- @arg res: The result of the open operation (Handle or Error).
--
-- @details
--   If Success: Assigns a new FD, stores Handle in vmFds, pushes FD.
--   If Error: Pushes -1 to the stack.
--
registerHandle :: Either IOException Handle -> VirtualMachine ()
registerHandle :: Either IOException Handle -> VirtualMachine ()
registerHandle (Left IOException
_) = VMValue -> VirtualMachine ()
stackPush (IntValue -> VMValue
VInt (Int32 -> IntValue
I32 (-Int32
1)))
registerHandle (Right Handle
h) = do
    vm <- StateT VMState IO VMState
forall s (m :: * -> *). MonadState s m => m s
get
    put vm { vmFds = Map.insert (nextFd vm) h (vmFds vm),
        nextFd = nextFd vm + 1 }
    stackPush (VInt (I32 (fromIntegral (nextFd vm))))

-- | Implements OPEN instruction (Opcode 0xB0).
--
-- @arg StackTop: Mode (Int) - 0:Read, 1:Write, 2:Append.
-- @arg StackNext: Path (String).
--
-- @details
--   Pops path and mode, calls tryOpen, and pushes the resulting FD.
--   Safety: Returns -1 on failure instead of crashing.
--
instOpen :: VirtualMachine ()
instOpen :: VirtualMachine ()
instOpen = do
    mode <- Int -> IOMode
getIoMode (Int -> IOMode) -> (VMValue -> Int) -> VMValue -> IOMode
forall b c a. (b -> c) -> (a -> b) -> a -> c
. VMValue -> Int
valueToInt (VMValue -> IOMode)
-> StateT VMState IO VMValue -> StateT VMState IO IOMode
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> StateT VMState IO VMValue
stackPop
    path <- valueToString <$> stackPop
    res <- tryOpen (T.unpack path) mode
    registerHandle res

-- | Wrapper to attempt reading from a handle.
--
-- @arg h: The file handle.
-- @arg size: Number of bytes to read (-1 for all).
--
-- @details
--   Dispatches between 'hGetContents' (full read) and 'readChars'
--   (partial read). Catches IOExceptions.
--
tryRead :: Handle -> Int -> VirtualMachine (Either IOException String)
tryRead :: Handle -> Int -> VirtualMachine (Either IOException FilePath)
tryRead Handle
h (-1) = IO (Either IOException FilePath)
-> VirtualMachine (Either IOException FilePath)
forall a. IO a -> StateT VMState IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either IOException FilePath)
 -> VirtualMachine (Either IOException FilePath))
-> IO (Either IOException FilePath)
-> VirtualMachine (Either IOException FilePath)
forall a b. (a -> b) -> a -> b
$ IO FilePath -> IO (Either IOException FilePath)
forall e a. Exception e => IO a -> IO (Either e a)
try (Handle -> IO FilePath
hGetContents Handle
h)
tryRead Handle
h Int
size = IO (Either IOException FilePath)
-> VirtualMachine (Either IOException FilePath)
forall a. IO a -> StateT VMState IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either IOException FilePath)
 -> VirtualMachine (Either IOException FilePath))
-> IO (Either IOException FilePath)
-> VirtualMachine (Either IOException FilePath)
forall a b. (a -> b) -> a -> b
$ IO FilePath -> IO (Either IOException FilePath)
forall e a. Exception e => IO a -> IO (Either e a)
try (Handle -> Int -> IO FilePath
readChars Handle
h Int
size)

-- | Helper to read N characters safely.
--
-- @arg h: The file handle.
-- @arg n: The exact number of characters to read.
--
-- @details
--   Recursive read that handles EOF gracefully.
--   Returns partial string if EOF is reached before N chars.
--
readChars :: Handle -> Int -> IO String
readChars :: Handle -> Int -> IO FilePath
readChars Handle
_ Int
0 = FilePath -> IO FilePath
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return []
readChars Handle
h Int
n = do
    eof <- Handle -> IO Bool
hIsEOF Handle
h
    case eof of
        Bool
True -> FilePath -> IO FilePath
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return []
        Bool
False -> (:) (Char -> FilePath -> FilePath)
-> IO Char -> IO (FilePath -> FilePath)
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Handle -> IO Char
hGetChar Handle
h IO (FilePath -> FilePath) -> IO FilePath -> IO FilePath
forall a b. IO (a -> b) -> IO a -> IO b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> Handle -> Int -> IO FilePath
readChars Handle
h (Int
n Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)

-- | Handles the result of a read operation.
--
-- @arg res: The result from tryRead (String or Error).
--
-- @details
--   Pushes the content string on success.
--   Pushes an empty list/string on failure.
--
handleReadResult :: Either IOException String -> VirtualMachine ()
handleReadResult :: Either IOException FilePath -> VirtualMachine ()
handleReadResult (Left IOException
_) = VMValue -> VirtualMachine ()
stackPush (Vector VMValue -> VMValue
VList Vector VMValue
forall a. Vector a
V.empty)
handleReadResult (Right FilePath
content) = VMValue -> VirtualMachine ()
stackPush (Text -> VMValue
stringToValue (FilePath -> Text
T.pack FilePath
content))

-- | Implements READ instruction (Opcode 0xB1).
--
-- @arg StackTop: Size (Int).
-- @arg StackNext: FD (Int).
--
-- @details
--   Reads 'Size' bytes from the file descriptor 'FD'.
--   Pushes the read content string to the stack.
--
instRead :: VirtualMachine ()
instRead :: VirtualMachine ()
instRead = do
    size <- VMValue -> Int
valueToInt (VMValue -> Int)
-> StateT VMState IO VMValue -> StateT VMState IO Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> StateT VMState IO VMValue
stackPop
    fd <- valueToInt <$> stackPop
    vm <- get
    case Map.lookup fd (vmFds vm) of
        Maybe Handle
Nothing -> VMValue -> VirtualMachine ()
stackPush (Vector VMValue -> VMValue
VList Vector VMValue
forall a. Vector a
V.empty)
        Just Handle
h -> Handle -> Int -> VirtualMachine (Either IOException FilePath)
tryRead Handle
h Int
size VirtualMachine (Either IOException FilePath)
-> (Either IOException FilePath -> VirtualMachine ())
-> VirtualMachine ()
forall a b.
StateT VMState IO a
-> (a -> StateT VMState IO b) -> StateT VMState IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Either IOException FilePath -> VirtualMachine ()
handleReadResult

-- | Wrapper to attempt writing to a handle.
--
-- @arg h: The file handle.
-- @arg c: The content string to write.
--
-- @details
--   Writes string and forces a flush (hFlush) to ensure
--   output is visible immediately (useful for terminals).
--
tryWrite :: Handle -> String -> VirtualMachine (Either IOException ())
tryWrite :: Handle -> FilePath -> VirtualMachine (Either IOException ())
tryWrite Handle
h FilePath
c = IO (Either IOException ())
-> VirtualMachine (Either IOException ())
forall a. IO a -> StateT VMState IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either IOException ())
 -> VirtualMachine (Either IOException ()))
-> IO (Either IOException ())
-> VirtualMachine (Either IOException ())
forall a b. (a -> b) -> a -> b
$ IO () -> IO (Either IOException ())
forall e a. Exception e => IO a -> IO (Either e a)
try (Handle -> FilePath -> IO ()
hPutStr Handle
h FilePath
c IO () -> IO () -> IO ()
forall a b. IO a -> IO b -> IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Handle -> IO ()
hFlush Handle
h)

-- | Implements WRITE instruction (Opcode 0xB2).
--
-- @arg StackTop: Content (String).
-- @arg StackNext: FD (Int).
--
-- @details
--   Writes content to the specified file descriptor.
--   Ignores operation if FD is invalid.
--
instWrite :: VirtualMachine ()
instWrite :: VirtualMachine ()
instWrite = do
    content <- VMValue -> Text
valueToString (VMValue -> Text)
-> StateT VMState IO VMValue -> StateT VMState IO Text
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> StateT VMState IO VMValue
stackPop
    fd <- valueToInt <$> stackPop
    vm <- get
    case Map.lookup fd (vmFds vm) of
        Just Handle
h -> Handle -> FilePath -> VirtualMachine (Either IOException ())
tryWrite Handle
h (Text -> FilePath
T.unpack Text
content) VirtualMachine (Either IOException ())
-> VirtualMachine () -> VirtualMachine ()
forall a b.
StateT VMState IO a -> StateT VMState IO b -> StateT VMState IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> () -> VirtualMachine ()
forall a. a -> StateT VMState IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()
        Maybe Handle
Nothing -> () -> VirtualMachine ()
forall a. a -> StateT VMState IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()

-- | Implements CLOSE instruction (Opcode 0xB3).
--
-- @arg StackTop: FD (Int).
--
-- @details
--   Closes the handle and removes the FD from the VM state map.
--   Safe to call on invalid FD (no-op).
--
instClose :: VirtualMachine ()
instClose :: VirtualMachine ()
instClose = do
    fd <- VMValue -> Int
valueToInt (VMValue -> Int)
-> StateT VMState IO VMValue -> StateT VMState IO Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> StateT VMState IO VMValue
stackPop
    vm <- get
    case Map.lookup fd (vmFds vm) of
        Just Handle
h -> IO () -> VirtualMachine ()
forall a. IO a -> StateT VMState IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (Handle -> IO ()
hClose Handle
h) VirtualMachine () -> VirtualMachine () -> VirtualMachine ()
forall a b.
StateT VMState IO a -> StateT VMState IO b -> StateT VMState IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> VMState -> Int -> VirtualMachine ()
removeFd VMState
vm Int
fd
        Maybe Handle
Nothing -> () -> VirtualMachine ()
forall a. a -> StateT VMState IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ()

-- | Removes a File Descriptor from the VM state.
--
-- @arg vm: The current VM State.
-- @arg fd: The File Descriptor to remove.
--
-- @details
--   Updates the Map in the state to delete the key.
--
removeFd :: VMState -> Int -> VirtualMachine ()
removeFd :: VMState -> Int -> VirtualMachine ()
removeFd VMState
vm Int
fd = VMState -> VirtualMachine ()
forall s (m :: * -> *). MonadState s m => s -> m ()
put VMState
vm { vmFds = Map.delete fd (vmFds vm) }

-- | Wrapper to attempt reading a line.
--
-- @arg h: The file handle.
--
-- @details
--   Uses hGetLine to read until newline.
--   Catches EOF or other IO errors.
--
tryInput :: Handle -> VirtualMachine (Either IOException String)
tryInput :: Handle -> VirtualMachine (Either IOException FilePath)
tryInput Handle
h = IO (Either IOException FilePath)
-> VirtualMachine (Either IOException FilePath)
forall a. IO a -> StateT VMState IO a
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (IO (Either IOException FilePath)
 -> VirtualMachine (Either IOException FilePath))
-> IO (Either IOException FilePath)
-> VirtualMachine (Either IOException FilePath)
forall a b. (a -> b) -> a -> b
$ IO FilePath -> IO (Either IOException FilePath)
forall e a. Exception e => IO a -> IO (Either e a)
try (Handle -> IO FilePath
hGetLine Handle
h)

-- | Implements INPUT instruction (Opcode 0xB4).
--
-- @arg StackTop: FD (Int).
--
-- @details
--   Reads a single line from the FD (useful for stdin).
--   Pushes the string content (without newline usually) to stack.
--
instInput :: VirtualMachine ()
instInput :: VirtualMachine ()
instInput = do
    fd <- VMValue -> Int
valueToInt (VMValue -> Int)
-> StateT VMState IO VMValue -> StateT VMState IO Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> StateT VMState IO VMValue
stackPop
    vm <- get
    case Map.lookup fd (vmFds vm) of
        Maybe Handle
Nothing -> VMValue -> VirtualMachine ()
stackPush (Vector VMValue -> VMValue
VList Vector VMValue
forall a. Vector a
V.empty)
        Just Handle
h -> Handle -> VirtualMachine (Either IOException FilePath)
tryInput Handle
h VirtualMachine (Either IOException FilePath)
-> (Either IOException FilePath -> VirtualMachine ())
-> VirtualMachine ()
forall a b.
StateT VMState IO a
-> (a -> StateT VMState IO b) -> StateT VMState IO b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Either IOException FilePath -> VirtualMachine ()
handleReadResult