module Language.ArrayForth.NativeProgram where
import           Control.Applicative        ((<$>), (<*>))
import           Control.Monad              ((<=<))
import           Data.Bits                  (shift, (.&.), (.|.))
import           Data.List.Split            (chunk, keepDelimsR, split, whenElt)
import           Data.String                (IsString, fromString)
import           Language.ArrayForth.Opcode
import           Language.ArrayForth.Parse
data Instrs = Instrs Opcode Opcode Opcode Opcode
            | Jump3 Opcode Opcode Opcode F18Word
            | Jump2 Opcode Opcode F18Word
            | Jump1 Opcode F18Word
            | Constant F18Word deriving (Eq)
instance Show Instrs where
  show (Instrs a b c d)   = unwords $ map show [a, b, c, d]
  show (Jump3 a b c addr) = unwords (map show [a, b, c]) ++ " " ++ show addr
  show (Jump2 a b addr)   = unwords (map show [a, b]) ++ " " ++ show addr
  show (Jump1 a addr)     = show a ++ " " ++ show addr
  show (Constant n)       = show n
  showList = (++) . unwords . map show
type NativeProgram = [Instrs]
splitWords :: (a -> Bool) -> [a] -> [[a]]
splitWords isNum = chunk 4 <=< split (keepDelimsR $ whenElt isNum)
readNativeProgram :: String -> Either ParseError NativeProgram
readNativeProgram = mapM go . splitWords isNumber . words
  where go [a, b, c, d] = do c' <- readOpcode c
                             if not $ isJump c'
                               then Instrs <$> op a <*> op b <*> op c <*> op3 d
                               else Jump3 <$> op a <*> op b <*> jump c <*> readWord d
        go [a, b, c]    = Jump2 <$> op a <*> jump b <*> readWord c
        go [a, b]       = Jump1 <$> jump a <*> readWord b
        go [a]          = Constant <$> readWord a
        go _            = error "Wrong number of instruction tokens!"
        wrap cond err str = do code <- readOpcode str
                               if cond code then Right code else Left $ err code
        op = wrap (not . isJump) $ NoAddr . show
        op3 = wrap slot3 $ NotSlot3 . show
        jump = wrap isJump $ NotJump . show
instance Read NativeProgram where
  readsPrec _ str = [(result, "")]
    where result = case readNativeProgram str of
            Right res -> res
            Left  err -> error $ show err
instance IsString NativeProgram where fromString = read
toBits :: Instrs -> F18Word
toBits (Instrs a b c d)   = fromOpcode a `shift` 13 .|. fromOpcode b `shift` 8  .|.
                            fromOpcode c `shift` 3  .|. fromOpcode d `shift` (2)
toBits (Jump3 a b c addr) = fromOpcode a `shift` 13 .|. fromOpcode b `shift` 8  .|.
                            fromOpcode c `shift` 3  .|. addr
toBits (Jump2 a b addr)   = fromOpcode a `shift` 13 .|. fromOpcode b `shift` 8  .|. addr
toBits (Jump1 a addr)     = fromOpcode a `shift` 13 .|. addr
toBits (Constant n)       = n
fromBits :: F18Word -> Instrs
fromBits n | isJump a  = Jump1 a     $ n .&. 0x3FF
           | isJump b  = Jump2 a b   $ n .&. 0xFF
           | isJump c  = Jump3 a b c $ n .&. 0x7
           | otherwise = Instrs a b c d
  where a = toOpcode $ n `shift` (13)
        b = toOpcode $ n `shift` (8) .&. 0x1F
        c = toOpcode $ n `shift` (3) .&. 0x1F
        d = toOpcode $ (n .&. 0x7) `shift` 2
toOpcodes :: Instrs -> [Opcode]
toOpcodes (Instrs a b c d) = [a, b, c, d]
toOpcodes (Jump3 a b c _)  = [a, b, c]
toOpcodes (Jump2 a b _)    = [a, b]
toOpcodes (Jump1 a _)      = [a]
toOpcodes Constant{}       = []
runningTime :: NativeProgram -> Double
runningTime = sum . map opcodeTime . reverse . dropWhile (== Nop) . reverse . concatMap toOpcodes