content: {-# LANGUAGE OverloadedStrings #-}
  
  -- | Support for CPP.
  module Ormolu.Processing.Cpp
    ( cppLines,
      eraseCppLines,
    )
  where
  
  import Data.IntSet (IntSet)
  import Data.IntSet qualified as IntSet
  import Data.Maybe (isJust)
  import Data.Text (Text)
  import Data.Text qualified as T
  
  -- | State of the CPP processor.
  data State
    = -- | Outside of CPP directives
      Outside
    | -- | In a conditional expression, with a positive nesting count
      InConditional Int
    | -- | In a continuation (after @\\@), but not in a conditional expression
      InContinuation
    deriving (Eq, Show)
  
  -- | Return an 'IntSet' containing all lines which are affected by CPP.
  cppLines :: Text -> IntSet
  cppLines input = IntSet.fromAscList $ go Outside (T.lines input `zip` [1 ..])
    where
      go _ [] = []
      go state ((line, i) : ls)
        | any for ["define ", "include ", "undef "] =
            i : go contState ls
        | any for ["ifdef ", "ifndef ", "if "] =
            let state' = case state of
                  InConditional nc -> InConditional (nc + 1)
                  _ -> InConditional 1
             in i : go state' ls
        | for "endif" =
            let state' = case state of
                  InConditional nc | nc > 1 -> InConditional (nc - 1)
                  _ -> Outside
             in i : go state' ls
        | otherwise =
            let is = case state of
                  Outside -> []
                  _ -> [i]
                state' = case state of
                  InContinuation -> contState
                  _ -> state
             in is <> go state' ls
        where
          for directive = isJust $ do
            s <- T.stripStart <$> T.stripPrefix "#" line
            T.stripPrefix directive s
          contState =
            if "\\" `T.isSuffixOf` line && not inConditional
              then InContinuation
              else Outside
            where
              inConditional = case state of
                InConditional {} -> True
                _ -> False
  
  -- | Replace all lines affected by CPP with blank lines.
  eraseCppLines :: Text -> Text
  eraseCppLines input =
    T.unlines . fmap eraseCpp $ T.lines input `zip` [1 ..]
    where
      linesToErase = cppLines input
      eraseCpp (x, i) =
        if i `IntSet.member` linesToErase
          then "\n"
          else x
  