{-# LANGUAGE ScopedTypeVariables #-}
module Language.SSVM.Operations
  (push, pushS, pushD,
   pop, dup, swap, over,
   printStack, printCurrentDef, printF,
   add, sub, neg, mul, divide, absF,
   remF, cmpF,
   variable, recall, assign, readVar, define,
   allocArray, readArray, assignArray,
   goto, jumpIf,
   mark, getMark,
   input,
   step
  ) where

import Data.Data
import Data.Char
import Data.Array
import qualified Data.Map as M
import Control.Monad.State

import Language.SSVM.Types

-- | Increment PC
step :: VM ()
step = do
  st <- get
  let was = vmPC st
  put $ st {vmPC = was + 1}

-- | Change stack with given function
withStack :: (Stack -> Stack) -> VM ()
withStack fn = do
  st <- get
  let stk = vmStack st
  setStack (fn stk)

-- | Change stack with given monadic action
withStackM :: (Stack -> VM Stack) -> VM ()
withStackM fn = do
  st <- get
  let stk = vmStack st
  stk' <- fn stk
  setStack stk'

-- | Set VM stack
setStack :: Stack -> VM ()
setStack stk = do
  st <- get
  put $ st { vmStack = stk }

-- | Push value to the stack
push :: (StackType a) => a -> VM ()
push x = withStack (toStack x:)

-- | Push stack item to the stack
pushS :: StackItem -> VM ()
pushS x = withStack (x:)

-- | Add item to current definition
pushD :: StackItem -> VM ()
pushD x = do
  st <- get
  let def = vmCurrentDefinition st
  put $ st {vmCurrentDefinition = (x:def)}

-- | Empty current definition
endDef :: VM ()
endDef = do
  st <- get
  put $ st {vmCurrentDefinition = []}

-- | Drop stack head
-- (a -- )
pop :: VM ()
pop = withStackM pop'
  where
    pop' [] = fail "DROP on empty stack!"
    pop' (x:xs) = return xs

-- | Duplicate stack head
-- (a -- a a)
dup :: VM ()
dup = withStackM dup'
  where
    dup' [] = fail "DUP on empty stack!"
    dup' (x:xs) = return (x:x:xs)

-- | Swap two top items on the stack
-- (a b -- b a)
swap :: VM ()
swap = withStackM swap'
  where
    swap' [] = fail "SWAP on empty stack!"
    swap' [_] = fail "SWAP on single-element stack!"
    swap' (x:y:xs) = return (y:x:xs)

-- | (a b -- a b a)
over :: VM ()
over = withStackM over'
  where
    over' [] = fail "OVER on empty stack!"
    over' [_] = fail "OVER on single-element stack!"
    over' (x:y:xs) = return (y:x:y:xs)

-- | Print stack content
printStack :: VM ()
printStack = do
  stk <- gets vmStack
  lift $ putStrLn $ unwords $ map showPrint stk

-- | Print current definition
printCurrentDef :: VM ()
printCurrentDef = do
  def <- gets vmCurrentDefinition
  lift $ putStr "Current definition: "
  lift $ putStrLn $ unwords $ map showItem (reverse def)

-- | Get stack head (and drop it from stack)
-- (a -- )
getStack :: VM StackItem
getStack = do
  stk <- gets vmStack
  case stk of
    [] -> fail "Trying to get element from empty stack!"
    (x:xs) -> do
              setStack xs
              return x

-- | Get stack head (and drop it from stack)
getArg :: forall a. (StackType a) => VM a
getArg = do
  stk <- gets vmStack
  case stk of
    [] -> fail "Trying to get element from empty stack!"
    (x:xs) -> case fromStack x of
                Just r -> do
                          setStack xs
                          return r
                Nothing -> fail $ "Stack type error: got " ++ showType x ++
                                  " while expecting " ++ show (typeOf (undefined :: a))

-- | Run given function on stack head
-- (a -- f(a))
liftF :: (StackType a) => (a -> a) -> VM ()
liftF fn = do
  x <- getArg
  push (fn x)

-- | Run given operation on two top stack items
-- (a b -- a `op` b)
liftF2 :: (StackType a) => (a -> a -> a) -> VM ()
liftF2 op = do
  y <- getArg
  x <- getArg
  let result = x `op` y
  push result

add :: VM ()
add = liftF2 ((+) :: Integer -> Integer -> Integer)

sub :: VM ()
sub = liftF2 ((-) :: Integer -> Integer -> Integer)

neg :: VM ()
neg = liftF ((\x -> -x) :: Integer -> Integer)

absF :: VM ()
absF = liftF (abs :: Integer -> Integer)

mul :: VM ()
mul = liftF2 ((*) :: Integer -> Integer -> Integer)

divide :: VM ()
divide = liftF2 (div :: Integer -> Integer -> Integer)

remF :: VM ()
remF = liftF2 (mod :: Integer -> Integer -> Integer)

cmpF :: VM ()
cmpF = do
    y <- getStack
    x <- getStack
    case (x,y) of
      (SInteger a, SInteger b) -> push (cmp a b)
      (SString a, SString b) -> push (cmp a b)
      _ -> fail $ "Invalid types on CMP: " ++ showType x ++ ", " ++ showType y
  where
    cmp :: (Ord a) => a -> a -> Integer
    cmp a b = case compare a b of
               LT -> -1
               EQ -> 0
               GT -> 1

-- | Print stack head
-- (a -- )
printF :: VM ()
printF = do
  x <- getStack
  lift $ putStr $ showPrint x

-- | Define word
define :: VM ()
define = do
    ws <- gets vmCurrentDefinition
    endDef
    w <- getStack
    col <- getStack
    when (col /= SInstruction COLON) $
      fail $ "No COLON before DEFINE!"
    case w of
      SString name -> do
        st <- get
        dict <- gets vmDefinitions
        pc <- gets vmPC
        let start = pc - length ws
            dict' = M.insert name (Definition start $ reverse ws) dict
        put $ st {vmDefinitions = dict'}
      x -> fail $ "New word name is " ++ showType x ++ ", not String!"

-- | Recall word definition
recall :: String -> VM Definition
recall name = do
  dict <- gets vmDefinitions
  case M.lookup name dict of
    Nothing -> fail $ "Unknown word: " ++ name
    Just list -> return list

-- | Define variable
variable :: VM ()
variable = do
  name <- getArg
  col <- getStack
  when (col /= SInstruction COLON) $
    fail $ "No COLON before VARIABLE!"
  st <- get
  pc <- gets vmPC
  let n = vmNextVariable st
      dict = M.insert name (Definition (pc-1) [SInteger $ fromIntegral n]) (vmDefinitions st)
  put $ st {vmDefinitions = dict, vmNextVariable = n+1}

-- | Assign value to variable
-- (value variable-number -- )
assign :: VM ()
assign = do
  n <- getArg
  value <- getStack
  st <- get
  let vars = M.insert n value (vmVariables st)
  put $ st {vmVariables = vars}

-- | Read variable value
-- (variable-number -- value)
readVar :: VM ()
readVar = do
  n <- getArg
  vars <- gets vmVariables
  case M.lookup n vars of
    Nothing -> fail $ "Trying to read variable before assignment: #" ++ show n
    Just value -> pushS value

-- | Allocate an array
-- (size variable-number -- )
allocArray :: VM ()
allocArray = do
  a  <- getArg
  sz <- getArg :: VM Int
  st <- get
  let arr = listArray (1,sz) (replicate sz $ SInteger 0)
      vars = M.insert a (SArray arr) (vmVariables st)
  put $ st {vmVariables = vars}

-- | Assign value to array item.
-- (value array-or-variable-number index -- )
assignArray :: VM ()
assignArray = do
  i <- getArg
  a <- getStack
  value <- getStack
  case a of
    SInteger n -> do
      st <- get
      let v = fromIntegral n :: Int
          vars = vmVariables st
      case M.lookup v vars of
        Just (SArray arr) -> do
                      let vars' = M.insert v (SArray (arr // [(i, value)])) vars
                      put $ st {vmVariables = vars'}
        Just x ->  fail $ "[!]: variable type is " ++ showType x ++ ", not Array!"
        Nothing -> fail $ "Trying to assign array item before array allocation!"
    SArray arr ->  push $ arr // [(i, value)]
    _ ->           fail $ "[!]: second argument is not array nor variable number, but " ++ showType a

-- | Read item from array.
-- (array-or-variable-number index -- value)
readArray :: VM ()
readArray = do
  i <- getArg
  a <- getStack
  case a of
    SInteger n -> do
      st <- get
      let v = fromIntegral n :: Int
          vars = vmVariables st
      case M.lookup v vars of
        Just (SArray arr) -> pushS (arr ! i)
        Just x ->   fail $ "[@]: variable type is " ++ showType x ++ ", not Array!"
        Nothing ->  fail "Trying to read array item before array allocation!"
    SArray arr ->   pushS (arr ! i)
    _ ->            fail $ "[@]: second argument is not array nor variable number, but " ++ showType a

-- | Read value from stdin
-- ( -- value)
input :: VM ()
input = do
  str <- lift getLine
  if all isDigit str
    then pushS (SInteger $ read str)
    else pushS (SString str)

-- | Mark at current PC
-- ( -- pc)
mark :: VM ()
mark = do
  pc <- gets vmPC
  pushS (SInteger $ fromIntegral pc)

-- | Go to named instruction
branch :: Int -> VM ()
branch n = do
  st <- get
  put $ st {vmPC = n}

-- | Get PC from stack
-- (pc -- )
goto :: VM ()
goto = do
  n <- getArg :: VM Integer
  branch (fromIntegral n)

-- | Jump to given address if condition is satisfied
jumpIf :: (Integer -> Bool) -> VM ()
jumpIf test = do
  addr <- getArg :: VM Integer
  cond <- getStack
  case cond of
    SInteger i -> if test i
                    then branch (fromIntegral addr)
                    else step
    _ -> fail $ "Condition value is " ++ showType cond ++ ", not Integer!"

-- | Get mark by name
-- ( -- pc)
getMark :: [Marks] -> String -> VM ()
getMark [] _ = fail $ "Internal error: getMark with empty marks stack!"
getMark (marks:_) name = do
  case M.lookup name marks of
    Just x -> pushS (SInteger $ fromIntegral x)
    Nothing -> fail $ "Undefined mark: " ++ name