-- | Parsing of built-in Michelson macros.

module Michelson.Parser.Macro
  ( macro
  -- * These are handled separately to have better error messages
  , pairMac
  , ifCmpMac
  , mapCadrMac
  ) where

import Prelude hiding (note, try)

import Text.Megaparsec (customFailure, notFollowedBy, try)
import Text.Megaparsec.Char.Lexer (decimal)

import Michelson.Macro (CadrStruct(..), Macro(..), PairStruct(..), ParsedOp(..))
import qualified Michelson.Macro as Macro
import Michelson.Parser.Annotations
import Michelson.Parser.Error
import Michelson.Parser.Helpers
import Michelson.Parser.Instr
import Michelson.Parser.Lexer
import Michelson.Parser.Type
import Michelson.Parser.Types (Parser)
import Michelson.Untyped (T(..), Type(..), noAnn)
import Util.Alternative (someNE)
import Util.Positive

macro :: Parser ParsedOp -> Parser Macro
macro opParser =
      word' "CASE" CASE <*> someNE ops
  <|> symbol' "TAG" *> tagMac
  <|> symbol' "ACCESS" *> accessMac
  <|> symbol' "SET " *> setMac
  <|> word' "CONSTRUCT" CONSTRUCT <*> someNE ops
  <|> word' "VIEW" VIEW <*> ops
  <|> word' "VOID" VOID <*> ops
  <|> word' "CMP" CMP <*> cmpOp <*> noteDef
  <|> word' "IF_SOME" IF_SOME <*> ops <*> ops
  <|> word' "IF_RIGHT" IF_RIGHT <*> ops <*> ops
  <|> word' "FAIL" FAIL
  <|> word' "ASSERT_CMP" ASSERT_CMP <*> cmpOp
  <|> word' "ASSERT_NONE" ASSERT_NONE
  <|> word' "ASSERT_SOME" ASSERT_SOME
  <|> word' "ASSERT_LEFT" ASSERT_LEFT
  <|> word' "ASSERT_RIGHT" ASSERT_RIGHT
  <|> word' "ASSERT_" ASSERTX <*> cmpOp
  <|> word' "ASSERT" ASSERT
  <|> do string' "DI"; n <- num "I"; symbol' "P"; DIIP (n + 1) <$> ops
  <|> do string' "DU"; n <- num "U"; symbol' "P"; DUUP (n + 1) <$> noteDef
  <|> unpairMac
  <|> cadrMac
  <|> setCadrMac
  where
   ops = ops' opParser
   num str = fromIntegral . length <$> some (string' str)

pairMac :: Parser Macro
pairMac = do
  a <- pairMacInner
  symbol' "R"
  (tn, vn, fns) <- permute3Def noteDef note (some note)
  let ps = Macro.mapLeaves ((noAnn,) <$> fns) a
  return $ PAPAIR ps tn vn

pairMacInner :: Parser PairStruct
pairMacInner = do
  string' "P"
  l <- (string' "A" $> F (noAnn, noAnn)) <|> pairMacInner
  r <- (string' "I" $> F (noAnn, noAnn)) <|> pairMacInner
  return $ P l r

unpairMac :: Parser Macro
unpairMac = do
  string' "UN"
  a <- pairMacInner
  symbol' "R"
  (vns, fns) <- permute2Def (some note) (some note)
  return $ UNPAIR (Macro.mapLeaves (zip vns fns) a)

cadrMac :: Parser Macro
cadrMac = lexeme $ do
  string' "C"
  a <- some $ try $ cadrInner <* notFollowedBy (string' "R")
  b <- cadrInner
  symbol' "R"
  (vn, fn) <- notesVF
  return $ CADR (a ++ pure b) vn fn

cadrInner :: Parser CadrStruct
cadrInner = (string' "A" $> A) <|> (string' "D" $> D)

{-# ANN module ("HLint: ignore Reduce duplication" :: Text) #-}
setCadrMac :: Parser Macro
setCadrMac = do
  string' "SET_C"
  a <- some cadrInner
  symbol' "R"
  (v, f) <- notesVF
  return $ SET_CADR a v f

mapCadrMac :: Parser ParsedOp -> Parser Macro
mapCadrMac opParser = do
  string' "MAP_C"
  a <- some cadrInner
  symbol' "R"
  (v, f) <- notesVF
  MAP_CADR a v f <$> ops' opParser

ifCmpMac :: Parser ParsedOp -> Parser Macro
ifCmpMac opParser =
  word' "IFCMP" IFCMP <*> cmpOp <*> noteDef <*> ops' opParser <*> ops' opParser

tagMac :: Parser Macro
tagMac = do
  idx <- decimal
  mSpace
  ty <- type_
  let utys = unrollUnion ty []
  when (fromIntegral idx >= length utys) $
    customFailure $ WrongTagArgs idx (lengthNE utys)
  return $ TAG idx utys
  where
  unrollUnion ty =
    case ty of
      Type (TOr _ _ l r) _ -> unrollUnion l . toList . unrollUnion r
      _ -> (ty :|)

accessMac :: Parser Macro
accessMac = do
  idx <- decimal
  mSpace
  size <- positive
  when (idx >= unPositive size) $
    customFailure $ WrongAccessArgs idx size
  return $ ACCESS idx size

setMac :: Parser Macro
setMac = do
  idx <- decimal
  mSpace
  size <- positive
  when (idx >= unPositive size) $
    customFailure $ WrongSetArgs idx size
  return $ SET idx size