packages feed

inferno-types-0.1.0.0: src/Inferno/Types/Syntax.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Inferno.Types.Syntax
  ( Ident (..),
    ExtIdent (..),
    ImplExpl (..),
    Import (..),
    ModuleName (..),
    InfixFixity (..),
    Fixity (..),
    Comment (..),
    IStr (..),
    OpsTable,
    SomeIStr (..),
    toEitherList,
    fromEitherList,
    Lit (..),
    Pat (..),
    PatF (..),
    TV (..),
    BaseType (..),
    InfernoType (..),
    Expr
      ( ..,
        Var_,
        OpVar_,
        TypeRep_,
        Enum_,
        App_,
        Lam_,
        Let_,
        Lit_,
        InterpolatedString_,
        If_,
        Op_,
        PreOp_,
        Tuple_,
        One_,
        Empty_,
        Assert_,
        Case_,
        Array_,
        ArrayComp_,
        Bracketed_,
        RenameModule_,
        OpenModule_
      ),
    BlockUtils (..),
    ElementPosition (..),
    TList (..),
    SigVar (..),
    SourcePos (..),
    Scoped (..),
    Dependencies (..),
    arbitraryName,
    collectArrs,
    extractArgsAndPrettyPrint,
    tListToList,
    tListFromList,
    sigVarToIdent,
    sigVarToExpr,
    patternToExpr,
    incSourceCol,
    fromScoped,
    rws,
    punctuate',
    hideInternalIdents,
    substInternalIdents,
    getIdentifierPositions,
  )
where

import Control.Applicative (liftA, liftA2, liftA3)
import Control.DeepSeq (NFData (..))
import Control.Monad (replicateM)
import Data.Aeson (FromJSON (..), FromJSONKey (..), FromJSONKeyFunction (FromJSONKeyTextParser), ToJSON (..), ToJSONKey (..))
import Data.Aeson.Types (toJSONKeyText)
import Data.Bifunctor.TH (deriveBifunctor)
import Data.Data (Constr, Data (..), Typeable, gcast1, mkConstr, mkDataType)
import qualified Data.Data as Data
import Data.Functor.Foldable (ana, cata, project)
import Data.Functor.Foldable.TH (makeBaseFunctor)
import Data.Hashable (Hashable (hashWithSalt))
import Data.Int (Int64)
import qualified Data.IntMap as IntMap
import Data.List.NonEmpty (NonEmpty ((:|)), toList)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Serialize (Serialize)
import qualified Data.Serialize as Serialize
import qualified Data.Set as Set
import Data.String (IsString)
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Word (Word64)
import GHC.Generics (Generic)
import Inferno.Utils.Prettyprinter (renderPretty)
import Numeric (showHex)
import Prettyprinter
  ( Doc,
    Pretty (pretty),
    align,
    concatWith,
    enclose,
    flatAlt,
    group,
    hardline,
    indent,
    lbracket,
    line,
    line',
    lparen,
    nest,
    rbracket,
    rparen,
    sep,
    vsep,
    (<+>),
  )
import qualified Prettyprinter.Internal as Pretty
import Test.QuickCheck (Arbitrary (..), Gen, elements, listOf, oneof, recursivelyShrink, shrinkNothing, sized, suchThat)
import Test.QuickCheck.Arbitrary.ADT (ToADTArbitrary)
import Test.QuickCheck.Instances.Text ()
import Text.Megaparsec (Pos, SourcePos (..), mkPos, unPos)
import Text.Read (readMaybe)

newtype TV = TV {unTV :: Int}
  deriving stock (Eq, Ord, Show, Data, Generic)
  deriving newtype (ToJSON, FromJSON, ToJSONKey, FromJSONKey, NFData, Hashable, Arbitrary, Serialize)
  deriving anyclass (ToADTArbitrary)

data BaseType
  = TInt
  | TDouble
  | TWord16
  | TWord32
  | TWord64
  | TText
  | TTime
  | TTimeDiff
  | TResolution
  | TEnum Text (Set.Set Ident)
  deriving (Show, Eq, Ord, Data, Generic, ToJSON, FromJSON, NFData, ToADTArbitrary)

instance Arbitrary BaseType where
  shrink = shrinkNothing
  arbitrary =
    oneof $
      (TEnum <$> (Text.pack <$> arbitrary) <*> (Set.fromList <$> listOf arbitrary))
        : ( map
              pure
              [ TInt,
                TDouble,
                TWord16,
                TWord32,
                TWord64,
                TText,
                TTime,
                TTimeDiff,
                TResolution
              ]
          )

instance Serialize BaseType where
  get =
    Serialize.getInt8 >>= \case
      0 -> pure TInt
      1 -> pure TDouble
      2 -> pure TWord16
      3 -> pure TWord32
      4 -> pure TWord64
      5 -> pure TText
      6 -> pure TTime
      7 -> pure TTimeDiff
      8 -> pure TResolution
      _ -> do
        nm <- Serialize.get
        ids <- Serialize.get
        pure $ TEnum (Text.decodeUtf8 nm) $ Set.fromList $ map (Ident . Text.decodeUtf8) ids

  put = \case
    TInt -> Serialize.putInt8 0
    TDouble -> Serialize.putInt8 1
    TWord16 -> Serialize.putInt8 2
    TWord32 -> Serialize.putInt8 3
    TWord64 -> Serialize.putInt8 4
    TText -> Serialize.putInt8 5
    TTime -> Serialize.putInt8 6
    TTimeDiff -> Serialize.putInt8 7
    TResolution -> Serialize.putInt8 8
    TEnum nm ids -> do
      Serialize.putInt8 9
      Serialize.put $ Text.encodeUtf8 nm
      Serialize.put $ map (Text.encodeUtf8 . unIdent) $ Set.toList ids

instance Hashable BaseType where
  hashWithSalt s TInt = hashWithSalt s (1 :: Int)
  hashWithSalt s TDouble = hashWithSalt s (2 :: Int)
  hashWithSalt s TWord16 = hashWithSalt s (3 :: Int)
  hashWithSalt s TWord32 = hashWithSalt s (4 :: Int)
  hashWithSalt s TWord64 = hashWithSalt s (5 :: Int)
  hashWithSalt s TText = hashWithSalt s (6 :: Int)
  hashWithSalt s TTime = hashWithSalt s (7 :: Int)
  hashWithSalt s TTimeDiff = hashWithSalt s (8 :: Int)
  hashWithSalt s TResolution = hashWithSalt s (9 :: Int)
  hashWithSalt s (TEnum nm cs) = hashWithSalt s (10 :: Int, nm, Set.toList cs)

data InfernoType
  = TVar TV
  | TBase BaseType
  | TArr InfernoType InfernoType
  | TArray InfernoType
  | TSeries InfernoType
  | TOptional InfernoType
  | TTuple (TList InfernoType)
  | TRep InfernoType
  deriving (Show, Eq, Ord, Data, Generic, ToJSON, FromJSON, NFData, Hashable, ToADTArbitrary)
  deriving anyclass (Serialize)

instance Arbitrary InfernoType where
  shrink = recursivelyShrink
  arbitrary = sized arbitrarySized
    where
      arbitraryVar =
        TVar <$> arbitrary

      arbitraryArr n =
        TArr
          <$> (arbitrarySized $ n `div` 3)
          <*> (arbitrarySized $ n `div` 3)

      arbitraryTTuple n =
        oneof
          [ pure $ TTuple TNil,
            TTuple <$> (TCons <$> (arbitrarySized $ n `div` 3) <*> (arbitrarySized $ n `div` 3) <*> listOf (arbitrarySized $ n `div` 3))
          ]

      arbitraryBase = TBase <$> arbitrary

      arbitraryRest n = do
        constr <- elements [TArray, TSeries, TOptional, TRep]
        constr <$> (arbitrarySized $ n `div` 3)

      arbitrarySized 0 =
        oneof
          [ arbitraryVar,
            arbitraryBase
          ]
      arbitrarySized n =
        oneof
          [ arbitraryVar,
            arbitraryBase,
            arbitraryArr n,
            arbitraryTTuple n,
            arbitraryRest n
          ]

punctuate' :: Doc ann -> [Doc ann] -> [Doc ann]
punctuate' _ [] = []
punctuate' _ [d] = [d]
punctuate' p (d : ds) = (d <+> p) : punctuate' p ds

collectArrs :: InfernoType -> [InfernoType]
collectArrs (TArr ty1 ty2) = ty1 : collectArrs ty2
collectArrs t = [t]

letters :: [Text]
letters = map Text.pack $ [1 ..] >>= flip replicateM ['a' .. 'z']

instance Pretty TV where
  pretty (TV i) = "'" <> pretty (letters !! i)

instance Pretty BaseType where
  pretty = \case
    TInt -> "int"
    TDouble -> "double"
    TWord16 -> "word16"
    TWord32 -> "word32"
    TWord64 -> "word64"
    TText -> "text"
    TTime -> "time"
    TTimeDiff -> "timeDiff"
    TResolution -> "resolution"
    TEnum t _ -> pretty t

instance Pretty InfernoType where
  pretty = \case
    TVar v -> pretty v
    TBase b -> pretty b
    t@(TArr _ _) ->
      let prettyType = align . sep . punctuate' "→"
       in prettyType $
            map (\t' -> case t' of TArr _ _ -> enclose lparen rparen $ pretty t'; _ -> pretty t') $
              collectArrs t
    TArray ty@(TVar _) -> "array of" <+> align (pretty ty)
    TArray ty@(TBase _) -> "array of" <+> align (pretty ty)
    TArray ty@(TTuple _) -> "array of" <+> align (pretty ty)
    TArray ty -> "array of" <+> align (enclose lparen rparen $ pretty ty)
    TSeries ty@(TVar _) -> "series of" <+> align (pretty ty)
    TSeries ty@(TBase _) -> "series of" <+> align (pretty ty)
    TSeries ty@(TTuple _) -> "series of" <+> align (pretty ty)
    TSeries ty -> "series of" <+> align (enclose lparen rparen $ pretty ty)
    TOptional ty@(TVar _) -> "option of" <+> align (pretty ty)
    TOptional ty@(TBase _) -> "option of" <+> align (pretty ty)
    TOptional ty@(TTuple _) -> "option of" <+> align (pretty ty)
    TOptional ty -> "option of" <+> align (enclose lparen rparen $ pretty ty)
    TTuple tys -> Pretty.tupled (map pretty $ tListToList tys)
    TRep ty@(TVar _) -> "rep of" <+> align (pretty ty)
    TRep ty@(TBase _) -> "rep of" <+> align (pretty ty)
    TRep ty@(TTuple _) -> "rep of" <+> align (pretty ty)
    TRep ty -> "rep of" <+> align (enclose lparen rparen $ pretty ty)

incSourceCol :: SourcePos -> Int -> SourcePos
incSourceCol pos 0 = pos
incSourceCol (SourcePos n l c) i = SourcePos n l (c <> mkPos i)

rws :: [Text] -- list of reserved words
rws = ["if", "then", "else", "let", "module", "in", "match", "with", "Some", "None", "assert", "fun", "infixr", "infixl", "infix", "enum", "open"]

newtype Ident = Ident {unIdent :: Text}
  deriving stock (Eq, Ord, Show, Data, Generic)
  deriving newtype (ToJSON, FromJSON, ToJSONKey, FromJSONKey, IsString, NFData, Hashable)
  deriving anyclass (ToADTArbitrary)

arbitraryName :: Gen Text
arbitraryName =
  ( (\a as -> Text.pack $ a : as)
      <$> (elements ['a' .. 'z'])
      <*> (listOf $ elements $ ['0' .. '9'] ++ ['a' .. 'z'] ++ ['_'])
  )
    `suchThat` (\i -> not $ i `elem` rws)

instance Arbitrary Ident where
  shrink = shrinkNothing
  arbitrary = Ident <$> arbitraryName

newtype ModuleName = ModuleName {unModuleName :: Text}
  deriving stock (Eq, Ord, Show, Data, Generic)
  deriving newtype (ToJSON, FromJSON, IsString)
  deriving anyclass (ToADTArbitrary)

instance Arbitrary ModuleName where
  shrink = shrinkNothing
  arbitrary = ModuleName <$> arbitraryName

class ElementPosition a where
  elementPosition :: SourcePos -> a -> (SourcePos, SourcePos)

instance ElementPosition Ident where
  elementPosition pos (Ident a) = (pos, incSourceCol pos $ Text.length a)

instance ElementPosition ModuleName where
  elementPosition pos (ModuleName a) = (pos, incSourceCol pos $ Text.length a)

instance ElementPosition (Maybe Ident) where
  elementPosition pos = \case
    Just i -> elementPosition pos i
    Nothing -> (pos, incSourceCol pos 1)

-- | An extended identifier; either an internal (e.g., var$4) or a regular variable
newtype ExtIdent = ExtIdent (Either Int Text)
  deriving (Show, Eq, Ord, Data, Generic)
  deriving newtype (ToJSON, FromJSON)

instance Arbitrary ExtIdent where
  shrink = shrinkNothing
  arbitrary =
    ExtIdent <$> oneof [Left <$> (arbitrary `suchThat` ((<) 0)), Right <$> arbitraryName]

instance ToJSONKey ExtIdent where
  toJSONKey = toJSONKeyText $ \case
    ExtIdent (Left i) -> "var$" <> (Text.pack $ show i)
    ExtIdent (Right k) -> "reg$" <> k

instance FromJSONKey ExtIdent where
  fromJSONKey = FromJSONKeyTextParser $ \t ->
    case Text.take 4 t of
      "var$" -> case readMaybe $ Text.unpack $ Text.drop 4 t of
        Just i -> pure $ ExtIdent $ Left i
        Nothing -> fail "Could not read internal var"
      "reg$" -> pure $ ExtIdent $ Right $ Text.drop 4 t
      _ -> fail "Invalid ExtIdent key"

data ImplExpl = Impl ExtIdent | Expl ExtIdent
  deriving (Show, Eq, Ord, Data, Generic, ToJSON, FromJSON)

instance Pretty ExtIdent where
  pretty (ExtIdent i) = case i of
    Left n -> "var$" <> pretty n
    Right x -> pretty x

instance Pretty ImplExpl where
  pretty = \case
    Impl a -> "?" <> pretty a
    Expl a -> pretty a

instance ElementPosition ImplExpl where
  elementPosition pos = \case
    Impl (ExtIdent (Left _)) -> (pos, pos)
    Impl (ExtIdent (Right a)) -> (pos, incSourceCol pos $ Text.length a + 1)
    Expl (ExtIdent (Left _)) -> (pos, pos)
    Expl (ExtIdent (Right a)) -> (pos, incSourceCol pos $ Text.length a)

data Fixity = InfixOp InfixFixity | PrefixOp deriving (Show, Eq, Ord, Data, Generic, ToJSON, FromJSON)

data InfixFixity = NoFix | LeftFix | RightFix deriving (Show, Eq, Ord, Data, Generic, ToJSON, FromJSON)

instance ToJSON Pos where
  toJSON = toJSON . unPos

deriving instance ToJSON SourcePos

instance FromJSON Pos where
  parseJSON = fmap mkPos . parseJSON

deriving instance FromJSON SourcePos

data Comment pos
  = LineComment pos Text pos
  | BlockComment pos Text pos
  deriving (Show, Eq, Ord, Data, Generic, Functor, Foldable, ToJSON, FromJSON)

instance Pretty (Comment a) where
  pretty = \case
    LineComment _ str _ -> ("//" <+> pretty str)
    BlockComment _ str _ -> encloseComment $ map pretty $ Text.splitOn "\n" $ Text.strip str
    where
      encloseComment ds = case ds of
        [] -> "/*  */"
        [d] -> "/*" <+> d <+> "*/"
        _ -> hardVcat (zipWith (<>) ("/* " : repeat mempty) ds) <> " */"

      hardVcat :: [Doc ann] -> Doc ann
      hardVcat = concatWith (\x y -> x <> hardline <> y)

data Lit
  = LInt Int64
  | LDouble Double
  | LText Text
  | LHex Word64
  deriving (Show, Eq, Ord, Data, Generic, ToJSON, FromJSON)

instance Pretty Lit where
  pretty = \case
    LInt i -> if i < 0 then "(" <> pretty i <> ")" else pretty i
    LDouble d -> if d < 0 then "(" <> pretty d <> ")" else pretty d
    LText t -> pretty $ show t
    LHex w -> "0x" <> (pretty $ showHex w "")

instance ElementPosition Lit where
  elementPosition pos l = (pos, incSourceCol pos $ length $ show $ pretty l)

data TList a = TNil | TCons a a [a]
  deriving (Show, Eq, Ord, Functor, Foldable, Data, Generic, ToJSON, FromJSON, NFData, Hashable, ToADTArbitrary)
  deriving anyclass (Serialize)

instance Arbitrary a => Arbitrary (TList a) where
  arbitrary =
    oneof
      [ pure TNil,
        TCons <$> arbitrary <*> arbitrary <*> listOf arbitrary
      ]

instance Traversable TList where
  {-# INLINE traverse #-} -- so that traverse can fuse
  traverse f = \case
    TNil -> pure TNil
    TCons x y zs -> liftA3 TCons (f x) (f y) (traverse f zs)

tListToList :: TList a -> [a]
tListToList = \case
  TNil -> []
  TCons a b cs -> a : b : cs

tListFromList :: [a] -> TList a
tListFromList = \case
  [] -> TNil
  (a : b : cs) -> TCons a b cs
  _ -> error "undefined TList"

data IStr (f :: Bool) e where
  ISEmpty :: IStr 'True e
  ISStr :: Text -> IStr 'True e -> IStr 'False e
  ISExpr :: Typeable f => e -> IStr f e -> IStr 'True e

instance (Typeable f, Data e) => Data (IStr f e) where
  gfoldl _ z ISEmpty = z ISEmpty
  gfoldl k z (ISStr s xs) = z ISStr `k` s `k` xs
  gfoldl k z (ISExpr e xs) = z ISExpr `k` e `k` xs

  gunfold _ _ _ =
    error $
      "Cannot derive a gunfold instance without unsafeCoerce.\n"
        <> "If this function is needed, try uncommenting the lines below. However, this definition might not be correct."

  -- where
  --   gunfold' :: forall c. (forall b r. Data b => c (b -> r) -> c r) -> (forall r. r -> c r) -> Constr -> c (IStr f e)
  --   gunfold' k z c = case constrIndex c of
  --     1 -> unsafeCoerce $ z ISEmpty
  --     2 -> unsafeCoerce $ k (k (z ISStr) :: Typeable f => c (IStr 'True e -> IStr 'False e))
  --     _ -> unsafeCoerce $ k (k (z ISExpr) :: Typeable f => c (IStr f e -> IStr 'True e))

  toConstr ISEmpty = con_ISEmpty
  toConstr (ISStr _ _) = con_ISStr
  toConstr (ISExpr _ _) = con_ISExpr

  dataTypeOf _ = ty_IStr
  dataCast1 f = gcast1 f

con_ISEmpty, con_ISStr, con_ISExpr :: Constr
con_ISEmpty = mkConstr ty_IStr "ISEmpty" [] Data.Prefix
con_ISStr = mkConstr ty_IStr "ISStr" [] Data.Prefix
con_ISExpr = mkConstr ty_IStr "ISExpr" [] Data.Prefix

ty_IStr :: Data.DataType
ty_IStr = mkDataType "Inferno.Syntax.IStr" [con_ISEmpty, con_ISStr, con_ISExpr]

deriving instance Show e => Show (IStr f e)

deriving instance Functor (IStr f)

deriving instance Foldable (IStr f)

instance Traversable (IStr f) where
  {-# INLINE traverse #-} -- so that traverse can fuse
  traverse f = \case
    ISEmpty -> pure ISEmpty
    ISStr s xs -> liftA (ISStr s) (traverse f xs)
    ISExpr e xs -> liftA2 ISExpr (f e) (traverse f xs)

data SomeIStr e = forall f. Typeable f => SomeIStr (IStr f e)

instance Data e => Data (SomeIStr e) where
  gfoldl k z (SomeIStr xs) = z SomeIStr `k` xs

  gunfold _ _ _ =
    error $
      "Cannot derive a gunfold instance without unsafeCoerce.\n"
        <> "If this function is needed, try uncommenting the lines below. However, this definition might not be correct."

  -- where
  --   gunfold' :: forall c. (forall b r. Data b => c (b -> r) -> c r) -> (forall r. r -> c r) -> Constr -> c (SomeIStr e)
  --   gunfold' k z _ = k (z SomeIStr :: c (IStr 'False e -> SomeIStr e))

  toConstr _ = con_SomeIStr
  dataTypeOf _ = ty_SomeIStr
  dataCast1 f = gcast1 f

con_SomeIStr :: Constr
con_SomeIStr = mkConstr ty_SomeIStr "SomeIStr" [] Data.Prefix

ty_SomeIStr :: Data.DataType
ty_SomeIStr = mkDataType "Inferno.Syntax.SomeIStr" [con_SomeIStr]

deriving instance Show e => Show (SomeIStr e)

instance Eq e => Eq (SomeIStr e) where
  (SomeIStr ISEmpty) == (SomeIStr ISEmpty) = True
  (SomeIStr (ISStr s1 xs)) == (SomeIStr (ISStr s2 ys)) =
    (s1 == s2) && (SomeIStr xs) == (SomeIStr ys)
  (SomeIStr (ISExpr e1 xs)) == (SomeIStr (ISExpr e2 ys)) =
    (e1 == e2) && (SomeIStr xs) == (SomeIStr ys)
  _ == _ = False

instance Ord e => Ord (SomeIStr e) where
  compare (SomeIStr ISEmpty) (SomeIStr ISEmpty) = EQ
  compare (SomeIStr ISEmpty) _ = LT
  compare _ (SomeIStr ISEmpty) = GT
  compare (SomeIStr (ISStr _ _)) (SomeIStr (ISExpr _ _)) = LT
  compare (SomeIStr (ISExpr _ _)) (SomeIStr (ISStr _ _)) = GT
  compare (SomeIStr (ISStr s xs)) (SomeIStr (ISStr t ys)) = case compare s t of
    EQ -> compare (SomeIStr xs) (SomeIStr ys)
    other -> other
  compare (SomeIStr (ISExpr e xs)) (SomeIStr (ISExpr f ys)) = case compare e f of
    EQ -> compare (SomeIStr xs) (SomeIStr ys)
    other -> other

deriving instance Functor SomeIStr

deriving instance Foldable SomeIStr

toEitherList :: SomeIStr e -> [Either Text e]
toEitherList = \case
  SomeIStr ISEmpty -> []
  SomeIStr (ISStr s ys) -> Left s : toEitherList (SomeIStr ys)
  SomeIStr (ISExpr e ys) -> Right e : toEitherList (SomeIStr ys)

fromEitherList :: [Either Text e] -> SomeIStr e
fromEitherList = \case
  [] -> SomeIStr (ISEmpty :: IStr 'True e)
  Left s : xs -> case fromEitherList xs of
    SomeIStr ISEmpty -> SomeIStr $ ISStr s ISEmpty
    SomeIStr (ISStr s' xs') -> SomeIStr $ ISStr (s <> s') xs'
    SomeIStr rest@(ISExpr _ _) -> SomeIStr $ ISStr s rest
  Right e : xs -> case fromEitherList xs of
    SomeIStr rest -> SomeIStr $ ISExpr e rest

instance FromJSON e => FromJSON (SomeIStr e) where
  parseJSON = fmap fromEitherList . parseJSON

instance ToJSON e => ToJSON (SomeIStr e) where
  toJSON = toJSON . toEitherList

instance Traversable SomeIStr where
  {-# INLINE traverse #-} -- so that traverse can fuse
  traverse f = \case
    SomeIStr (ISEmpty :: IStr f a) -> pure $ SomeIStr (ISEmpty :: IStr f b)
    SomeIStr (ISStr s xs) -> do
      res <- traverse f xs
      pure $ case res of
        ISEmpty -> SomeIStr $ ISStr s ISEmpty
        rest@(ISExpr _ _) -> SomeIStr $ ISStr s rest
    SomeIStr (ISExpr e xs) -> do
      e' <- f e
      res <- traverse f xs
      pure $ SomeIStr $ ISExpr e' res

data Import pos
  = IVar pos Ident
  | IOpVar pos Ident
  | IEnum
      pos -- position of `enum`
      pos -- position of ident
      Ident
  | ICommentAbove (Comment pos) (Import pos)
  | ICommentAfter (Import pos) (Comment pos)
  | ICommentBelow (Import pos) (Comment pos)
  deriving (Show, Eq, Ord, Functor, Foldable, Generic, Data, ToJSON, FromJSON)

makeBaseFunctor ''Import

data Scoped a = LocalScope | Scope a
  deriving (Show, Eq, Ord, Functor, Foldable, Traversable, Data, Generic, ToJSON, FromJSON)

fromScoped :: a -> Scoped a -> a
fromScoped d = \case
  Scope a -> a
  LocalScope -> d

data Expr hash pos
  = Var
      pos
      hash
      (Scoped ModuleName)
      ImplExpl
  | OpVar pos hash (Scoped ModuleName) Ident -- infix ops like `+` used as prefix, i.e. `(+)`
  | TypeRep pos InfernoType
  | Enum pos hash (Scoped ModuleName) Ident
  | App (Expr hash pos) (Expr hash pos)
  | Lam
      pos -- position of `fun`
      ( NonEmpty
          ( pos, -- position of variable,
            Maybe ExtIdent
          )
      )
      pos -- position of `->`
      (Expr hash pos)
  | Let
      pos -- position of `let`
      pos -- position of variable
      ImplExpl
      pos -- position of `=`
      (Expr hash pos)
      pos -- position of `in`
      (Expr hash pos)
  | Lit pos Lit
  | InterpolatedString
      pos -- position of string start
      (SomeIStr (pos, Expr hash pos, pos))
      pos -- position of string end
  | If
      pos -- position of `if`
      (Expr hash pos)
      pos -- position of then`
      (Expr hash pos)
      pos -- position of `else`
      (Expr hash pos)
  | Op
      (Expr hash pos)
      pos -- position of operator
      hash
      (Int, InfixFixity) -- level and fixity of the operaror
      (Scoped ModuleName)
      Ident
      (Expr hash pos)
  | PreOp
      pos -- position of operator
      hash
      Int -- level of the operaror
      (Scoped ModuleName)
      Ident
      (Expr hash pos)
  | Tuple
      pos -- position of `(`
      (TList (Expr hash pos, Maybe pos))
      pos -- position of `)`
      -- NOTE: the frontend syntax is Some/None but internally we use one/empty for legacy reasons
  | One
      pos -- position of `Some`
      (Expr hash pos)
  | Empty pos -- position of `None`
  | Assert
      pos -- position of `assert`
      (Expr hash pos)
      pos -- position of `in`
      (Expr hash pos)
  | Case
      pos -- position of `match`
      (Expr hash pos)
      pos -- position of `{`
      ( NonEmpty
          ( pos, -- position of `|`
            Pat hash pos,
            pos, -- position of `->`
            Expr hash pos
          )
      )
      pos -- position of `}`
  | Array
      pos -- position of `[`
      [ ( Expr hash pos,
          Maybe pos -- position of `,`
        )
      ]
      pos -- position of `]`
  | ArrayComp
      pos -- position of `[`
      (Expr hash pos)
      pos -- position of `|`
      ( NonEmpty
          ( pos, -- position of identifier
            Ident,
            pos, -- position of `<-`
            Expr hash pos,
            Maybe pos -- position of `,`
          )
      )
      ( Maybe
          ( pos, -- position of `if`
            Expr hash pos
          )
      )
      pos -- position of `]`
  | CommentAbove
      (Comment pos)
      (Expr hash pos)
  | CommentAfter
      (Expr hash pos)
      (Comment pos)
  | CommentBelow
      (Expr hash pos)
      (Comment pos)
  | Bracketed pos (Expr hash pos) pos
  | RenameModule
      pos -- pos of new name
      ModuleName
      pos -- pos of old name
      ModuleName
      pos -- pos of `in`
      (Expr hash pos)
  | OpenModule
      pos
      hash
      ModuleName
      [(Import pos, Maybe pos)]
      pos -- pos of `in`
      (Expr hash pos)
  deriving (Show, Eq, Ord, Functor, Foldable, Generic, Data, ToJSON, FromJSON)

{-# COMPLETE
  Var_,
  OpVar_,
  TypeRep_,
  Enum_,
  App_,
  Lam_,
  Let_,
  Lit_,
  InterpolatedString_,
  If_,
  Op_,
  PreOp_,
  Tuple_,
  One_,
  Empty_,
  Assert_,
  Case_,
  Array_,
  ArrayComp_,
  CommentAbove,
  CommentAfter,
  CommentBelow,
  Bracketed_,
  RenameModule_,
  OpenModule_
  #-}

pattern Var_ :: forall hash pos. hash -> Scoped ModuleName -> ImplExpl -> Expr hash pos
pattern Var_ h ns x <- Var _ h ns x

pattern OpVar_ :: forall hash pos. hash -> Scoped ModuleName -> Ident -> Expr hash pos
pattern OpVar_ h ns x <- OpVar _ h ns x

pattern TypeRep_ :: forall hash pos. InfernoType -> Expr hash pos
pattern TypeRep_ ty <- TypeRep _ ty

pattern Enum_ :: forall hash pos. hash -> Scoped ModuleName -> Ident -> Expr hash pos
pattern Enum_ h ns x <- Enum _ h ns x

pattern App_ :: forall hash pos. Expr hash pos -> Expr hash pos -> Expr hash pos
pattern App_ e1 e2 <- App e1 e2

pattern Lam_ :: forall hash pos. NonEmpty (pos, Maybe ExtIdent) -> Expr hash pos -> Expr hash pos
pattern Lam_ xs e <- Lam _ xs _ e

pattern Let_ :: forall hash pos. ImplExpl -> Expr hash pos -> Expr hash pos -> Expr hash pos
pattern Let_ x e1 e2 <- Let _ _ x _ e1 _ e2

pattern Lit_ :: forall hash pos. Lit -> Expr hash pos
pattern Lit_ l <- Lit _ l

pattern InterpolatedString_ :: forall hash pos. SomeIStr (pos, Expr hash pos, pos) -> Expr hash pos
pattern InterpolatedString_ xs <- InterpolatedString _ xs _

pattern If_ :: forall hash pos. Expr hash pos -> Expr hash pos -> Expr hash pos -> Expr hash pos
pattern If_ c t f <- If _ c _ t _ f

pattern Op_ :: forall hash pos. Expr hash pos -> hash -> Scoped ModuleName -> Ident -> Expr hash pos -> Expr hash pos
pattern Op_ e1 h ns op e2 <- Op e1 _ h _ ns op e2

pattern PreOp_ :: forall hash pos. hash -> Scoped ModuleName -> Ident -> Expr hash pos -> Expr hash pos
pattern PreOp_ h ns op e <- PreOp _ h _ ns op e

pattern Tuple_ :: forall hash pos. TList (Expr hash pos, Maybe pos) -> Expr hash pos
pattern Tuple_ xs <- Tuple _ xs _

pattern One_ :: forall hash pos. Expr hash pos -> Expr hash pos
pattern One_ e <- One _ e

pattern Empty_ :: forall hash pos. Expr hash pos
pattern Empty_ <- Empty _

pattern Assert_ :: forall hash pos. Expr hash pos -> Expr hash pos -> Expr hash pos
pattern Assert_ c e <- Assert _ c _ e

pattern Case_ :: forall hash pos. Expr hash pos -> NonEmpty (pos, Pat hash pos, pos, Expr hash pos) -> Expr hash pos
pattern Case_ e xs <- Case _ e _ xs _

pattern Array_ :: forall hash pos. [(Expr hash pos, Maybe pos)] -> Expr hash pos
pattern Array_ xs <- Array _ xs _

pattern ArrayComp_ ::
  forall hash pos.
  Expr hash pos ->
  NonEmpty (pos, Ident, pos, Expr hash pos, Maybe pos) ->
  Maybe (pos, Expr hash pos) ->
  Expr hash pos
pattern ArrayComp_ e xs c <- ArrayComp _ e _ xs c _

pattern Bracketed_ :: forall hash pos. Expr hash pos -> Expr hash pos
pattern Bracketed_ e <- Bracketed _ e _

pattern RenameModule_ :: forall hash pos. ModuleName -> ModuleName -> Expr hash pos -> Expr hash pos
pattern RenameModule_ n1 n2 e <- RenameModule _ n1 _ n2 _ e

pattern OpenModule_ :: forall hash pos. ModuleName -> [(Import pos, Maybe pos)] -> Expr hash pos -> Expr hash pos
pattern OpenModule_ n1 ns e <- OpenModule _ _ n1 ns _ e

data Pat hash pos
  = PVar pos (Maybe Ident)
  | PEnum pos hash (Scoped ModuleName) Ident
  | PLit pos Lit
  | POne pos (Pat hash pos)
  | PEmpty pos
  | PTuple pos (TList (Pat hash pos, Maybe pos)) pos
  | PCommentAbove
      (Comment pos)
      (Pat hash pos)
  | PCommentAfter
      (Pat hash pos)
      (Comment pos)
  | PCommentBelow
      (Pat hash pos)
      (Comment pos)
  deriving (Show, Eq, Ord, Functor, Foldable, Data, Generic, ToJSON, FromJSON)

makeBaseFunctor ''Pat

makeBaseFunctor ''Expr

deriveBifunctor ''Pat

deriveBifunctor ''Expr

patternToExpr :: Pat () () -> Expr () ()
patternToExpr = \case
  PVar _ Nothing -> Var () () LocalScope $ Expl $ ExtIdent $ Right "_"
  PVar _ (Just (Ident i)) -> Var () () LocalScope $ Expl $ ExtIdent $ Right i
  PEnum _ _ modNm i -> Enum () () modNm i
  PLit _ l -> Lit () l
  POne _ p -> One () $ patternToExpr p
  PEmpty _ -> Empty ()
  PTuple _ ps _ -> Tuple () (fmap (\(pat, pos) -> (patternToExpr pat, pos)) ps) ()
  PCommentAbove c p -> CommentAbove c $ patternToExpr p
  PCommentAfter p c -> CommentAfter (patternToExpr p) c
  PCommentBelow p c -> CommentBelow (patternToExpr p) c

getIdentifierPositions :: Ident -> Expr a SourcePos -> [(SourcePos, SourcePos)]
getIdentifierPositions (Ident i) = cata go
  where
    go :: ExprF a SourcePos [(SourcePos, SourcePos)] -> [(SourcePos, SourcePos)]
    go = \case
      VarF pos _ _ v@(Expl (ExtIdent (Right a))) -> if i == a then let (sPos, ePos) = elementPosition pos v in [(sPos, ePos)] else []
      rest -> foldr (++) [] rest

class BlockUtils f where
  blockPosition :: f SourcePos -> (SourcePos, SourcePos)
  removeComments :: f pos -> f pos
  hasLeadingComment :: f pos -> Bool
  hasTrailingComment :: f pos -> Bool
  renameModule :: Scoped ModuleName -> f pos -> f pos

instance BlockUtils Comment where
  blockPosition = \case
    LineComment s _ e -> (s, e)
    BlockComment s _ e -> (s, e)

  removeComments = id
  hasLeadingComment _ = True
  hasTrailingComment _ = True
  renameModule _ = id

instance BlockUtils Import where
  blockPosition p = cata go p
    where
      go = \case
        IVarF pos v -> elementPosition pos v
        IOpVarF pos (Ident i) -> (pos, incSourceCol pos $ Text.length i + 2)
        IEnumF pos1 pos2 (Ident i) -> (pos1, incSourceCol pos2 $ Text.length i)
        ICommentAboveF c (_, pos2) -> let (pos1, _) = blockPosition c in (pos1, pos2)
        ICommentAfterF (pos1, _) c -> let (_, pos2) = blockPosition c in (pos1, pos2)
        ICommentBelowF (pos1, _) c -> let (_, pos2) = blockPosition c in (pos1, pos2)

  removeComments = ana $ \case
    ICommentAbove _ p -> project $ removeComments p
    ICommentAfter p _ -> project $ removeComments p
    ICommentBelow p _ -> project $ removeComments p
    other -> project other
  renameModule _ = id

  hasLeadingComment = head . cata go
    where
      go = \case
        ICommentAboveF _ _ -> [True]
        rest -> foldr (++) [False] rest

  hasTrailingComment = last . cata go
    where
      go = \case
        ICommentAfterF _ _ -> [True]
        ICommentBelowF _ _ -> [True]
        rest -> foldl (++) [False] rest

instance BlockUtils (Pat hash) where
  blockPosition p = cata go p
    where
      go :: PatF hash SourcePos (SourcePos, SourcePos) -> (SourcePos, SourcePos)
      go = \case
        PVarF pos v -> elementPosition pos v
        PEnumF pos _ ns (Ident i) -> (pos, incSourceCol pos $ Text.length i + 1 + (fromScoped 0 $ (+ 1) . Text.length . unModuleName <$> ns))
        PLitF pos l -> elementPosition pos l
        PEmptyF pos -> (pos, incSourceCol pos 5)
        POneF pos1 (_, pos2) -> (pos1, pos2)
        PTupleF pos1 _ pos2 -> (pos1, incSourceCol pos2 1)
        PCommentAboveF c (_, pos2) -> let (pos1, _) = blockPosition c in (pos1, pos2)
        PCommentAfterF (pos1, _) c -> let (_, pos2) = blockPosition c in (pos1, pos2)
        PCommentBelowF (pos1, _) c -> let (_, pos2) = blockPosition c in (pos1, pos2)

  removeComments = ana $ \case
    PCommentAbove _ p -> project $ removeComments p
    PCommentAfter p _ -> project $ removeComments p
    PCommentBelow p _ -> project $ removeComments p
    other -> project other

  renameModule newNs = ana $ \case
    PEnum pos hash _ns i -> project $ PEnum pos hash newNs i
    other -> project other

  hasLeadingComment = head . cata go
    where
      go :: PatF hash pos [Bool] -> [Bool]
      go = \case
        PCommentAboveF _ _ -> [True]
        rest -> foldr (++) [False] rest

  hasTrailingComment = last . cata go
    where
      go :: PatF hash pos [Bool] -> [Bool]
      go = \case
        PCommentAfterF _ _ -> [True]
        PCommentBelowF _ _ -> [True]
        rest -> foldl (++) [False] rest

instance BlockUtils (Expr hash) where
  blockPosition e = cata go e
    where
      go :: ExprF hash SourcePos (SourcePos, SourcePos) -> (SourcePos, SourcePos)
      go = \case
        VarF pos _ ns v -> let (sPos, ePos) = elementPosition pos v in (sPos, incSourceCol ePos $ fromScoped 0 $ (+ 1) . Text.length . unModuleName <$> ns)
        OpVarF pos _ ns v -> let (sPos, ePos) = elementPosition pos v in (sPos, incSourceCol ePos $ fromScoped 2 $ (+ 3) . Text.length . unModuleName <$> ns)
        EnumF pos _ ns (Ident i) -> (pos, incSourceCol pos $ Text.length i + 1 + (fromScoped 0 $ (+ 1) . Text.length . unModuleName <$> ns))
        AppF (pos1, _) (_, pos2) -> (pos1, pos2)
        LamF pos1 _ _ (_, pos2) -> (pos1, pos2)
        LetF pos1 _ _ _ _ _ (_, pos2) -> (pos1, pos2)
        LitF pos l -> (pos, incSourceCol pos $ length $ show $ pretty l)
        InterpolatedStringF pos1 _ pos2 -> (pos1, pos2)
        IfF pos1 _ _ _ _ (_, pos2) -> (pos1, pos2)
        OpF (pos1, _) _ _ _ _ _ (_, pos2) -> (pos1, pos2)
        PreOpF pos1 _ _ _ _ (_, pos2) -> (pos1, pos2)
        TupleF pos1 _ pos2 -> (pos1, incSourceCol pos2 1)
        OneF pos1 (_, pos2) -> (pos1, pos2)
        EmptyF pos -> (pos, incSourceCol pos 5)
        AssertF pos1 _ _ (_, pos2) -> (pos1, pos2)
        CaseF pos1 _ _ _ pos2 -> (pos1, incSourceCol pos2 1)
        ArrayF pos1 _ pos2 -> (pos1, incSourceCol pos2 1)
        ArrayCompF pos1 _ _ _ _ pos2 -> (pos1, incSourceCol pos2 1)
        CommentAboveF c (_, pos2) -> let (pos1, _) = blockPosition c in (pos1, pos2)
        CommentAfterF (pos1, _) c -> let (_, pos2) = blockPosition c in (pos1, pos2)
        CommentBelowF (pos1, _) c -> let (_, pos2) = blockPosition c in (pos1, pos2)
        BracketedF pos1 _ pos2 -> (pos1, pos2)
        RenameModuleF pos1 _ _ _ _ (_, pos2) -> (pos1, pos2)
        OpenModuleF pos1 _ _ _ _ (_, pos2) -> (pos1, pos2)
        TypeRepF pos _ -> (pos, pos)

  removeComments = ana $ \case
    CommentAbove _ p -> project $ removeComments p
    CommentAfter p _ -> project $ removeComments p
    CommentBelow p _ -> project $ removeComments p
    Case p1 e1 p2 xs p3 ->
      project $
        Case
          p1
          (removeComments e1)
          p2
          (fmap (\(p4, pat, p5, e2) -> (p4, removeComments pat, p5, removeComments e2)) xs)
          p3
    other -> project other

  renameModule newNs = ana $ \e -> project $ case e of
    Var pos hash _ns i -> Var pos hash newNs i
    OpVar pos hash _ns i -> OpVar pos hash newNs i
    Enum pos hash _ns i -> Enum pos hash newNs i
    Op e1 p1 hash meta _ns op e2 -> Op e1 p1 hash meta newNs op e2
    Case p1 e1 p2 xs p3 ->
      Case
        p1
        (renameModule newNs e1)
        p2
        (fmap (\(p4, pat, p5, e2) -> (p4, renameModule newNs pat, p5, renameModule newNs e2)) xs)
        p3
    other -> other
  hasLeadingComment = head . cata go
    where
      go :: ExprF hash pos [Bool] -> [Bool]
      go = \case
        CommentAboveF _ _ -> [True]
        rest -> foldr (++) [False] rest

  hasTrailingComment = last . cata go
    where
      go :: ExprF hash pos [Bool] -> [Bool]
      go = \case
        CommentAfterF _ _ -> [True]
        CommentBelowF _ _ -> [True]
        rest -> foldl (++) [False] rest

collectApps :: Expr hash pos -> [Expr hash pos]
collectApps (App x@(App _ _) y) = collectApps x ++ [y]
collectApps (App x y) = [x, y]
collectApps _ = undefined

-- | Filter out any var$n/?var$n variables and their let/lambda bindings
-- This is used when pretty printing for the front-end, as we don't want the
-- users to see these auto-generated internal variables.
hideInternalIdents :: Expr hash pos -> Expr hash pos
hideInternalIdents = ana $ \case
  App e (Var _ _ _ (Impl (ExtIdent (Left _)))) -> project $ hideInternalIdents e
  App e (Var _ _ _ (Expl (ExtIdent (Left _)))) -> project $ hideInternalIdents e
  App e (TypeRep _ _) -> project $ hideInternalIdents e
  Let _ _ (Impl (ExtIdent (Left _))) _ _ _ e -> project $ hideInternalIdents e
  Let _ _ (Expl (ExtIdent (Left _))) _ _ _ e -> project $ hideInternalIdents e
  Lam p1 xs p2 e ->
    let filteredXs = flip NonEmpty.filter xs $ \case
          (_, Just (ExtIdent (Left _))) -> False
          _ -> True
     in case filteredXs of
          [] -> project $ hideInternalIdents e
          (x' : xs') -> project $ Lam p1 (x' NonEmpty.:| xs') p2 $ hideInternalIdents e
  other -> project other

-- | Extract the arguments of a script and pretty print the script body.
-- This hides the internal variable arguments.
extractArgsAndPrettyPrint :: Expr hash pos -> ([Maybe Ident], Text)
extractArgsAndPrettyPrint expr =
  extract [] (hideInternalIdents expr)
  where
    extract args = \case
      Lam _ (x :| xs) _ e -> extract (args <> map snd (x : xs)) e
      e -> (mapMaybe extIdentToIdent args, renderPretty e)
    -- Strip the runtime type rep arguments, and convert others to Ident
    extIdentToIdent = \case
      (Just (ExtIdent (Left _))) -> Nothing
      (Just (ExtIdent (Right i))) -> Just $ Just $ Ident {unIdent = i}
      Nothing -> Just Nothing

-- | Substitute every variable occurrence of `?var$i` with `var$j`
-- if `(i, Left j)` is in in the supplied map.
-- otherwise replace `?var$i` with `@t` if (i, Right t) \in m`
substInternalIdents :: Map.Map Int (Either Int InfernoType) -> Expr hash pos -> Expr hash pos
substInternalIdents m = ana $ \case
  Var pos h ns (Impl (ExtIdent (Left i))) ->
    project $ case Map.lookup i m of
      Just (Left j) -> Var pos h ns (Expl (ExtIdent (Left j)))
      Just (Right t) -> TypeRep pos t
      Nothing -> Var pos h ns (Impl (ExtIdent (Left i)))
  other -> project other

instance Pretty (Import a) where
  pretty = \case
    IVar _ (Ident x) -> pretty x
    IOpVar _ (Ident x) -> "(" <> pretty x <> ")"
    IEnum _ _ (Ident x) -> "enum" <+> pretty x
    ICommentAbove c e -> pretty c <> hardline <> pretty e
    ICommentAfter e c -> pretty e <+> pretty c
    ICommentBelow e c -> pretty e <> line <> pretty c

instance Pretty (Pat hash a) where
  pretty = \case
    PVar _ (Just (Ident x)) -> pretty x
    PVar _ Nothing -> "_"
    PEnum _ _ ns (Ident n) -> (fromScoped mempty $ (<> ".") . pretty . unModuleName <$> ns) <> "#" <> pretty n
    PLit _ l -> pretty l
    PTuple _ TNil _ -> "()"
    PTuple _ ps _ -> group $ (flatAlt "( " "(") <> prettyTuple True (tListToList ps)
    POne _ e -> "Some" <+> align (pretty e)
    PEmpty _ -> "None"
    PCommentAbove c e -> pretty c <> hardline <> pretty e
    PCommentAfter e c -> pretty e <+> pretty c
    PCommentBelow e c -> pretty e <> line <> pretty c
    where
      prettyTuple firstElement = \case
        [] -> mempty
        [(e, _)] ->
          align (pretty e)
            <> (if hasTrailingComment e then hardline <> ")" else flatAlt " )" ")")
        (e, _) : es ->
          (if not firstElement && hasLeadingComment e then line else mempty)
            <> align (pretty e)
            <> (if hasTrailingComment e then hardline else line')
            <> ", "
            <> prettyTuple False es

instance Pretty (Expr hash pos) where
  pretty = prettyPrec False 0

prettyPrec :: Bool -> Int -> Expr hash pos -> Doc ann
prettyPrec isBracketed prec expr =
  case expr of
    Var _ _ ns x -> (fromScoped mempty $ (<> ".") . pretty . unModuleName <$> ns) <> pretty x
    TypeRep _ ty -> "@" <> pretty ty
    OpVar _ _ ns (Ident x) -> (fromScoped mempty $ (<> ".") . pretty . unModuleName <$> ns) <> "(" <> pretty x <> ")"
    Enum _ _ ns (Ident n) -> (fromScoped mempty $ (<> ".") . pretty . unModuleName <$> ns) <> "#" <> pretty n
    App _ _ -> group $ nest 2 $ prettyApp $ collectApps expr
      where
        prettyAppAux m p = case m of
          Var _ _ _ _ -> p
          OpVar _ _ _ _ -> p
          Enum _ _ _ _ -> p
          Lit _ _ -> p
          InterpolatedString _ _ _ -> p
          Tuple _ _ _ -> p
          Empty _ -> p
          Array _ _ _ -> p
          ArrayComp _ _ _ _ _ _ -> p
          Bracketed _ _ _ -> p
          _ -> enclose lparen rparen $ if hasTrailingComment m then p <> hardline else p

        prettyApp = \case
          [] -> mempty
          [x] -> prettyAppAux x $ prettyPrec True 0 x
          (x : xs) -> (prettyAppAux x $ prettyPrec True 0 x) <> (if hasTrailingComment x then hardline else line) <> prettyApp xs
    Lam _ xs _ e ->
      let fun = "fun" <+> align (sep $ map (fromMaybe "_" . fmap pretty . snd) $ toList xs) <+> "->"
          body = align $ prettyPrec False 0 e
       in group $ nest 2 $ vsep [fun, body]
    Let _ _ x _ e1 _ e2 ->
      let letPretty = "let" <+> align (pretty x <+> "=" <+> align (prettyPrec False 0 e1))
          body = "in" <+> prettyPrec False 0 e2
       in letPretty <> (if hasTrailingComment e1 then hardline else line) <> body
    Lit _ l -> pretty l
    InterpolatedString _ istr _ -> enclose "`" "`" $
      group $
        cat' $
          case istr of
            SomeIStr ISEmpty -> []
            SomeIStr xs@(ISStr _ _) -> prettyISStr xs
            SomeIStr xs@(ISExpr _ _) -> "${" : prettyISExpr xs
      where
        prettyISExpr :: IStr 'True (a, Expr hash a, a) -> [Doc ann]
        prettyISExpr = \case
          ISEmpty -> []
          ISExpr (_, e, _) ISEmpty -> (indentE $ prettyPrec False 0 e) : if hasTrailingComment e then [hardline, "}"] else ["}"]
          ISExpr (_, e, _) xs@(ISExpr _ _) ->
            ((indentE $ prettyPrec False 0 e) : if hasTrailingComment e then [hardline, "}${"] else ["}${"]) ++ prettyISExpr xs
          ISExpr (_, e, _) (ISStr str xs@(ISExpr _ _)) ->
            let str' = vsepHard $ addToLast $ addToFirst $ map pretty $ Text.splitOn "\n" str
             in ((indentE $ prettyPrec False 0 e) : if hasTrailingComment e then [hardline, str'] else [str']) ++ prettyISExpr xs
          ISExpr (_, e, _) (ISStr str xs) ->
            let str' = vsepHard $ addToFirst $ map pretty $ Text.splitOn "\n" str
             in ((indentE $ prettyPrec False 0 e) : if hasTrailingComment e then [hardline, str'] else [str']) ++ prettyISExpr xs

        prettyISStr :: IStr 'False (a, Expr hash a, a) -> [Doc ann]
        prettyISStr = \case
          ISStr str ISEmpty -> [pretty str]
          ISStr str xs ->
            let str' = vsepHard $ addToLast $ map pretty $ Text.splitOn "\n" str
             in str' : prettyISExpr xs

        addToLast, addToFirst :: [Doc ann] -> [Doc ann]
        addToLast = \case
          [] -> []
          [s] -> [s <> "${"]
          s : xs -> s : addToLast xs
        addToFirst = \case
          [] -> []
          s : xs -> ("}" <> s) : xs
    If _ c _ t _ f ->
      let ifPretty = "if" <+> align (prettyPrec False 0 c)
          thenPretty = "then" <+> align (prettyPrec False 0 t)
          elsePretty = "else" <+> align (prettyPrec False 0 f)
       in nest 2 $
            ifPretty
              <> (if hasTrailingComment c then hardline else line)
              <> thenPretty
              <> (if hasTrailingComment t then hardline else line)
              <> elsePretty
    Op e1 _ _ (n, NoFix) ns (Ident op) e2 ->
      bracketWhen e2 (prec > n) $
        prettyOpAux (n + 1) e1 <> (if hasTrailingComment e1 then hardline else mempty)
          <+> prettyOp ns op
          <+> (if hasLeadingComment e2 then line else mempty) <> prettyOpAux (n + 1) e2
    Op e1 _ _ (n, LeftFix) ns (Ident op) e2 ->
      bracketWhen e2 (prec > n) $
        prettyOpAux n e1 <> (if hasTrailingComment e1 then hardline else mempty)
          <+> prettyOp ns op
          <+> (if hasLeadingComment e2 then line else mempty) <> prettyOpAux (n + 1) e2
    Op e1 _ _ (n, RightFix) ns (Ident op) e2 ->
      bracketWhen e2 (prec > n) $
        prettyOpAux (n + 1) e1 <> (if hasTrailingComment e1 then hardline else mempty)
          <+> prettyOp ns op
          <+> (if hasLeadingComment e2 then line else mempty) <> prettyOpAux n e2
    PreOp _ _ n ns (Ident op) e ->
      bracketWhen e (prec > n) $
        prettyOp ns op
          <+> (if hasLeadingComment e then line else mempty) <> prettyOpAux (n + 1) e
    Tuple _ TNil _ -> "()"
    Tuple _ xs _ -> group $ (flatAlt "( " "(") <> prettyTuple True (tListToList xs)
      where
        prettyTuple firstElement = \case
          [] -> mempty
          [(e, _)] ->
            align (prettyPrec False 0 e)
              <> (if hasTrailingComment e then hardline <> ")" else flatAlt " )" ")")
          (e, _) : es ->
            (if not firstElement && hasLeadingComment e then line else mempty)
              <> align (prettyPrec False 0 e)
              <> (if hasTrailingComment e then hardline else line')
              <> ", "
              <> prettyTuple False es
    One _ e -> "Some" <+> align (prettyPrec False 0 e)
    Empty _ -> "None"
    Assert _ c _ e ->
      let assertPretty = "assert" <+> align (prettyPrec False 0 c)
          body = (flatAlt "    in" "in") <+> align (prettyPrec False 0 e)
       in assertPretty <> (if hasTrailingComment c then hardline else line) <> body
    Case _ e_case _ patExprs _ ->
      group $
        nest 2 $
          vsep
            [ "match" <+> align (prettyPrec False 0 e_case <> if hasTrailingComment e_case then hardline else mempty) <+> "with" <+> "{",
              (align $ prettyCase True $ toList patExprs) <> flatAlt " }" "}"
            ]
      where
        prettyCase :: Bool -> [(a, Pat hash a, a, Expr hash a)] -> Doc ann
        prettyCase firstElement = \case
          [] -> mempty
          [(_, pat, _, e)] ->
            group
              ( "|"
                  <+> align
                    ( pretty pat <> (if hasTrailingComment pat then hardline else mempty)
                        <+> "->"
                          <> line
                          <> (prettyPrec False 0 e)
                    )
              )
              <> (if hasTrailingComment e then hardline else mempty)
          (_, pat, _, e) : es ->
            (if not firstElement && hasLeadingComment pat then hardline else mempty)
              <> group ("|" <+> align (pretty pat <> (if hasTrailingComment pat then hardline else mempty) <+> "->" <> line <> (prettyPrec False 0 e)))
              <> (if hasTrailingComment e then hardline else line)
              <> prettyCase False es
    Array _ [] _ -> "[]"
    Array _ xs _ -> group $ (flatAlt "[ " "[") <> prettyArray True xs
      where
        prettyArray firstElement = \case
          [] -> mempty
          [(e, _)] ->
            align (prettyPrec False 0 e)
              <> (if hasTrailingComment e then hardline <> "]" else flatAlt " ]" "]")
          (e, _) : es ->
            (if not firstElement && hasLeadingComment e then line else mempty)
              <> align (prettyPrec False 0 e)
              <> (if hasTrailingComment e then hardline else line')
              <> ", "
              <> prettyArray False es
    ArrayComp _ e_body _ args e_cond _ ->
      enclose lbracket rbracket $
        align $
          (align $ prettyPrec False 0 e_body <> if hasTrailingComment e_body then hardline else mempty) <+> align ("|" <+> (argsPretty $ toList args))
      where
        argsPretty = \case
          [] -> mempty
          [(_, Ident n, _, e, _)] ->
            pretty n
              <+> "<-"
              <+> align (prettyPrec False 0 e)
                <> case e_cond of
                  Just (_, c) -> (if hasTrailingComment e then hardline else line') <> "," <+> "if" <+> align (prettyPrec False 0 c) <> (if hasTrailingComment c then hardline else mempty)
                  Nothing -> if hasTrailingComment e then hardline else mempty
          (_, Ident n, _, e, _) : xs ->
            pretty n
              <+> "<-"
              <+> align (prettyPrec False 0 e)
                <> (if hasTrailingComment e then hardline else line')
                <> ", "
                <> argsPretty xs
    CommentAbove c e -> pretty c <> hardline <> prettyPrec isBracketed prec e
    CommentAfter e c -> prettyPrec isBracketed prec e <+> pretty c
    CommentBelow e c -> prettyPrec isBracketed prec e <> line <> pretty c
    Bracketed _ e _ -> enclose lparen rparen $ if hasTrailingComment e then prettyPrec True prec e <> hardline else prettyPrec True prec e
    RenameModule _ (ModuleName nNew) _ (ModuleName nOld) _ e ->
      let letPretty = "let" <+> align ("module" <+> pretty nNew <+> "=" <+> pretty nOld)
          body = (flatAlt " in" "in") <+> align (prettyPrec False 0 e)
       in letPretty <> line <> body
    OpenModule _ _ (ModuleName n) ns _ e ->
      "open"
        <+> pretty n
          <> ( case ns of
                 [] -> line
                 _ -> (align $ group $ (flatAlt "( " "(") <> prettyImports True (map fst ns)) <> (if hasTrailingComment $ fst (last ns) then hardline else line)
             )
          <> (flatAlt "  in" "in")
        <+> align (prettyPrec False 0 e)
      where
        prettyImports firstElement = \case
          [] -> mempty
          [i] ->
            align (pretty i)
              <> (if hasTrailingComment i then hardline <> ")" else flatAlt " )" ")")
          i : is ->
            (if not firstElement && hasLeadingComment i then line else mempty)
              <> align (pretty i)
              <> (if hasTrailingComment i then hardline else line')
              <> ", "
              <> prettyImports False is
  where
    indentE e = flatAlt (indent 2 e) e

    vsepHard :: [Doc ann] -> Doc ann
    vsepHard = concatWith (\x y -> x <> hardline <> y)

    cat' :: [Doc ann] -> Doc ann
    cat' [] = mempty
    cat' [x] = x
    cat' (x : Pretty.Line : xs) = x <> hardline <> cat' xs
    cat' (x : xs) = x <> line' <> cat' xs

    bracketWhen e b =
      if isBracketed
        then id
        else
          if b
            then (\x -> enclose lparen rparen $ x <> if hasTrailingComment e then hardline else mempty)
            else id

    prettyOp ns op = (fromScoped mempty $ (<> ".") . pretty . unModuleName <$> ns) <> pretty op

    prettyOpAux n e = case e of
      Var _ _ _ _ -> prettyPrec False n e
      OpVar _ _ _ _ -> prettyPrec False n e
      Enum _ _ _ _ -> prettyPrec False n e
      Lit _ _ -> prettyPrec False n e
      InterpolatedString _ _ _ -> prettyPrec False n e
      Tuple _ _ _ -> prettyPrec False n e
      Empty _ -> prettyPrec False n e
      Array _ _ _ -> prettyPrec False n e
      ArrayComp _ _ _ _ _ _ -> prettyPrec False n e
      Bracketed _ _ _ -> prettyPrec False n e
      Op _ _ _ _ _ _ _ -> prettyPrec False n e
      PreOp _ _ _ _ _ _ -> prettyPrec False n e
      _ -> enclose lparen rparen $ if hasTrailingComment e then prettyPrec False n e <> hardline else prettyPrec False n e

data SigVar = SigVar Text | SigOpVar Text deriving (Eq, Show, Data)

sigVarToIdent :: SigVar -> Ident
sigVarToIdent x = Ident $ case x of
  SigVar i -> i
  SigOpVar i -> i

sigVarToExpr :: Scoped ModuleName -> SigVar -> Expr () ()
sigVarToExpr modNm = \case
  SigVar i -> Var () () modNm $ Expl $ ExtIdent $ Right i
  SigOpVar i -> OpVar () () modNm $ Ident i

type OpsTable = IntMap.IntMap [(Fixity, Scoped ModuleName, Text)]

class Dependencies f hash where
  getDependencies :: Ord hash => f -> Set.Set hash

instance Dependencies (Pat hash pos) hash where
  getDependencies = cata $ \case
    PEnumF _ h _ _ -> Set.singleton h
    rest -> foldr Set.union mempty rest

instance Dependencies (Expr hash pos) hash where
  getDependencies = cata $ \case
    VarF _ h _ _ -> Set.singleton h
    OpVarF _ h _ _ -> Set.singleton h
    EnumF _ h _ _ -> Set.singleton h
    OpF _ _ h _ _ _ _ -> Set.singleton h
    rest -> foldr Set.union mempty rest