{-# LANGUAGE CPP, GADTs #-}

-- | Compatibility module to bridge the gap between megaparsec-5 and megaparsec-6
--
-- Import this instead of Text.Megaparsec, Text.Megaparsec.Char and
-- Text.Megaparsec.Text
module Text.Megaparsec.Compat
  (
  -- * Re-exports
    module Text.Megaparsec.Char
  , module Text.Megaparsec
  -- * Compatibility reimplementations
  , Parser
#if MIN_VERSION_megaparsec(6,0,0)
  , Dec
#endif
  , string
  -- * Custom error handling
  , CustomError
  , mkCustomError
  , addCustomError
  -- * Additional helpers
  , parseWithStart
  ) where

import qualified Data.Set as S
import           Data.Text (Text)
import           Text.Megaparsec.Char hiding (string)
import qualified Text.Megaparsec.Char as P
import qualified Data.List.NonEmpty as NE

#if MIN_VERSION_megaparsec(6,0,0)
import           Data.Set (Set)
import           Data.Void
import           Text.Megaparsec
#else
import           Text.Megaparsec.Prim
import           Text.Megaparsec hiding (string)
import qualified Data.Text as T
#endif

#if MIN_VERSION_megaparsec(6,0,0)
-- | Custom error type for when no custom errors are needed
type Dec = Void
#endif

-- | Same as the type in Text.Megaparsec.Text from megaparsec-5
type Parser = Parsec Dec Text

-- | Custom error type that mimics FancyError of megaparsec-6 but retains
-- information about unexpected and expected tokens.
#if MIN_VERSION_megaparsec(6,0,0)
data CustomError e = CustomError
  (Maybe (ErrorItem Char))      -- unexpected
  (Set (ErrorItem Char))        -- expected
  e                             -- custom error data
  deriving (Eq, Show, Ord)

instance ShowErrorComponent e => ShowErrorComponent (CustomError e) where
  showErrorComponent (CustomError us es e) =
    parseErrorTextPretty (TrivialError undefined us es :: ParseError Char Void)
    ++ showErrorComponent e
#else
data CustomError e = CustomError e
                   | CustomFail String
                   | CustomIndentation Ordering Pos Pos
  deriving (Eq, Ord, Show)

instance Ord e => ErrorComponent (CustomError e) where
  representFail = CustomFail
  representIndentation = CustomIndentation

instance ShowErrorComponent e => ShowErrorComponent (CustomError e) where
  showErrorComponent (CustomError e) = showErrorComponent e
  showErrorComponent (CustomFail msg) = msg
  showErrorComponent (CustomIndentation ord ref actual) =
    "incorrect indentation (got " ++ show (unPos actual) ++
    ", should be " ++ p ++ show (unPos ref) ++ ")"
    where p = case ord of
                LT -> "less than "
                EQ -> "equal to "
                GT -> "greater than "
#endif

-- | Wrap a custom error type into a 'ParseError'.
mkCustomError :: SourcePos -> e -> ParseError t (CustomError e)
#if MIN_VERSION_megaparsec(6,0,0)
mkCustomError pos custom = FancyError (neSingleton pos)
  (S.singleton (ErrorCustom (CustomError Nothing S.empty custom)))
#else
mkCustomError pos custom = ParseError (neSingleton pos) S.empty S.empty
  (S.singleton (CustomError custom))
#endif

-- | Add a custom error to an already existing error.
--
-- This retains the original information such as expected and unexpected tokens
-- as well as the source position.
addCustomError :: Ord e => ParseError Char (CustomError e) -> e -> ParseError Char (CustomError e)
#if MIN_VERSION_megaparsec(6,0,0)
addCustomError e custom = case e of
  TrivialError source us es ->
    FancyError source (S.singleton (ErrorCustom (CustomError us es custom)))
  FancyError source es ->
    FancyError source (S.insert (ErrorCustom (CustomError Nothing S.empty custom)) es)
#else
addCustomError e custom = e { errorCustom = S.insert (CustomError custom) (errorCustom e) }
#endif


-- | Like 'parse', but start at a specific source position instead of 0.
#if MIN_VERSION_megaparsec(6,0,0)
parseWithStart :: (Stream s, Ord e)
#else
parseWithStart :: (Stream s, ErrorComponent e)
#endif
               => Parsec e s a -> SourcePos -> s -> Either (ParseError (Token s) e) a
parseWithStart p pos = parse p' (sourceName pos)
  where p' = do setPosition pos; p


-- | Reimplementation of 'Text.Megaparsec.Char.string', but specialized to 'Text'.
#if MIN_VERSION_megaparsec(6,0,0)
string :: MonadParsec e s m => Tokens s -> m (Tokens s)
string = P.string
#else
string :: (MonadParsec e s m, Token s ~ Char) => Text -> m Text
string x = T.pack <$> P.string (T.unpack x)
#endif

neSingleton :: a -> NE.NonEmpty a
neSingleton x = x NE.:| []