-- First pass of compilation
-- - work out arities and fully apply functions / constructors (currying)
--     currying is problemmatic because we need to insert lambdas (η-expand) and
--     it breaks all of the de Bruijn indices
-- - expand metas (this is happening earlier)
-- - erase stuff (there is another copy that essentially does the same thing)
-- I could make names unique (e.q. on lambdas), but I might want that to vary per backend?
module Lib.CompileExp

import Prelude
import Lib.Common
import Lib.Types -- Name / Tm
import Lib.TopContext
import Lib.Prettier
import Lib.Util
import Lib.Ref2
import Data.SortedMap

-- REVIEW Separate pass for constructor magic?
-- ConCase SuccCon will be replaced by Default CLet,
-- but we would need to fix up zero, since we collapse extra constructors into a default case.
-- But should be ok becaon CLitAlt doesn't bind.

CExp : U

data CAlt : U where
  CConAlt : String → ConInfo → List String → CExp → CAlt
  -- REVIEW keep var name?
  CDefAlt : CExp -> CAlt
  -- literal
  CLitAlt : Literal -> CExp -> CAlt

data CExp : U where
  CBnd : Int -> CExp
  -- How is CLam different from CFun with one arg?
  CLam : Name -> CExp -> CExp
  CFun : List Name -> CExp -> CExp
  CApp : CExp -> List CExp -> Int -> CExp
  CCase : CExp -> List CAlt -> CExp
  CRef : QName -> CExp
  CMeta : Int -> CExp
  CLit : Literal -> CExp
  CLet : Name -> CExp -> CExp -> CExp
  CLetRec : Name -> CExp -> CExp -> CExp
  CErased : CExp
  -- Data / type constructor
  CConstr : Name -> List CExp -> CExp
  -- Raw javascript for `pfunc`
  CRaw : String -> List QName -> CExp
  -- Need this for magic Nat
  -- TODO - use for primitives too
  CPrimOp : String → CExp → CExp -> CExp

-- I'm counting Lam in the term for arity.  This matches what I need in
-- code gen.

lamArity : Tm -> Nat
lamArity (Lam _ _ _ _ t) = S (lamArity t)
lamArity _ = Z

-- This is how much we want to curry at top level
-- leading lambda Arity is used for function defs and metas
-- TODO - figure out how this will work with erasure
arityForName : {{Ref2 Defs St}} → FC -> QName -> M Nat
arityForName fc nm = do
  defs <- getRef Defs
  case lookupMap' nm defs of
    Nothing => error fc "Name \{show nm} not in scope"
    (Just Axiom) => pure Z
    (Just (TCon arity strs)) => pure $ cast arity
    (Just (DCon _ k str)) => pure $ cast k
    (Just (Fn t)) => pure $ lamArity t
    (Just (PrimTCon arity)) => pure $ cast arity
    (Just (PrimFn t arity used)) => pure arity

any : ∀ a. (a → Bool) → List a → Bool
any f Nil = False
any f (x :: xs) = if f x then True else any f xs

-- apply an expression at an arity to a list of args
-- CApp will specify any missing args, for eta conversion later
-- and any extra args get individual CApp.
apply : CExp -> List CExp -> SnocList CExp -> Nat -> M CExp
-- out of args, make one up (fix that last arg)
apply t Nil acc (S k) =
  pure $ CApp t (acc <>> Nil) (1 + cast k)
apply t (x :: xs) acc (S k)  =  apply t xs (acc :< x) k
-- once we hit zero, we fold the rest
apply t ts acc Z = go (CApp t (acc <>> Nil) 0) ts
  where
    go : CExp -> List CExp -> M CExp
    -- drop zero arg call
    go (CApp t Nil 0) args = go t args
    go t Nil = pure t
    go t (arg :: args) = go (CApp t (arg :: Nil) 0) args

lookupDef : {{Ref2 Defs St}} → FC → QName → M Def
lookupDef fc nm = do
  defs <- getRef Defs
  case lookupMap' nm defs of
    Nothing => error fc "\{show nm} not in scope"
    Just def => pure def

compileTerm : {{Ref2 Defs St}} → Tm -> M CExp
compileTerm (Bnd _ k) = pure $ CBnd k
-- need to eta expand to arity
compileTerm t@(Ref fc nm@(QN _ tag)) = do
  arity <- arityForName fc nm
  defs <- getRef Defs
  case arity of
    -- we don't need to curry functions that take one argument
    (S Z) => pure $ CRef nm
    Z =>
      case the (Maybe Def) $ lookupMap' nm defs of
        Just (DCon EnumCon _ _) => pure $ CLit $ LString tag
        Just (DCon ZeroCon _ _) => pure $ CLit $ LInt 0
        Just (DCon SuccCon _ _) =>
          pure $ CLam "x" $ CPrimOp "+" (CLit $ LInt 1) (CBnd 0)
        _ => pure $ CRef nm
    _ => apply (CRef nm) Nil Lin arity

compileTerm (Meta fc k) = error fc "Compiling meta \{show k}"
compileTerm (Lam _ nm _ _ t) = CLam nm <$> compileTerm t
compileTerm tm@(App _ _ _) = case funArgs tm of
  (Meta _ k, args) => do
        error (getFC tm) "Compiling an unsolved meta \{show tm}"
        -- info (getFC tm) "Compiling an unsolved meta \{show tm}"
        -- pure $ CApp (CRef "Meta\{show k}") Nil 0
  (t@(Ref fc nm), args) => do
        defs <- getRef Defs
        args' <- traverse compileTerm args
        arity <- arityForName fc nm
        case the (Maybe Def) $ lookupMap' nm defs of
          Just (DCon SuccCon _ _) => applySucc args'
          _ => apply (CRef nm) args' Lin arity
  (t, args) => do
        debug $ \ _ => "apply other \{render 90 $ pprint Nil t}"
        t' <- compileTerm t
        args' <- traverse compileTerm args
        apply t' args' Lin Z
  where
    applySucc : List CExp → M CExp
    applySucc Nil = pure $ CLam "x" $ CPrimOp "+" (CLit $ LInt 1) (CBnd 0)
    applySucc (t :: Nil) = pure $ CPrimOp "+" (CLit $ LInt 1) t
    applySucc _ = error emptyFC "overapplied Succ \{show tm}"
compileTerm (UU _) = pure $ CRef (QN Nil "U")
compileTerm (Pi _ nm icit rig t u) = do
  t' <- compileTerm t
  u' <- compileTerm u
  pure $ CApp (CRef (QN Nil "PiType")) (t' :: CLam nm u' :: Nil) 0
compileTerm (Case fc t alts) = do
  t' <- compileTerm t
  alts' <- for alts $ \case
    CaseDefault tm => CDefAlt <$> compileTerm tm
    -- we use the base name for the tag, some primitives assume this
    CaseCons qn@(QN ns nm) args tm => do
      defs <- getRef Defs
      def <- lookupDef emptyFC qn
      case def of
        DCon info _ _ => CConAlt nm info args <$> compileTerm tm
        _ => error fc "\{show nm} is not constructor"

    CaseLit lit tm => CLitAlt lit <$> compileTerm tm
  pure $ CCase t' $ fancyCons t' alts'
  where
    numAltP : CAlt → Bool
    numAltP (CConAlt _ SuccCon _ _) = True
    numAltP (CConAlt _ ZeroCon _ _) = True
    numAltP _ = False

    enumAlt : CAlt → CAlt
    enumAlt (CConAlt nm EnumCon args tm) = CLitAlt (LString nm) tm
    enumAlt alt = alt

    isInfo : ConInfo → CAlt → Bool
    isInfo needle (CConAlt _ info _ _)  = needle == info
    isInfo _ _ = False

    isDef : CAlt → Bool
    isDef (CDefAlt _) = True
    isDef _ = False

    getBody : CAlt → CExp
    getBody (CConAlt _ _ _ t) = t
    getBody (CLitAlt _ t) = t
    getBody (CDefAlt t) = t

    doNumCon : CExp → List CAlt → List CAlt
    doNumCon sc alts =
      let zeroAlt = case find (isInfo ZeroCon) alts of
            Just (CConAlt _ _ _ tm) => CLitAlt (LInt 0) tm :: Nil
            Just tm => fatalError "ERROR zeroAlt mismatch \{debugStr tm}"
            _ => case find isDef alts of
                  Just (CDefAlt tm) => CLitAlt (LInt 0) tm :: Nil
                  -- This happens if the zero alt is impossible
                  _ => Nil
      in
      let succAlt = case find (isInfo SuccCon) alts of
            Just (CConAlt _ _ _ tm) => CDefAlt (CLet "x" (CPrimOp "-" sc (CLit $ LInt 1)) tm) :: Nil
            Just tm => fatalError "ERROR succAlt mismatch \{debugStr tm}"
            _ => case find isDef alts of
              Just alt => alt :: Nil
              _ => Nil
      in zeroAlt ++ succAlt

    fancyCons : CExp → List CAlt → List CAlt
    fancyCons sc alts =
      if any numAltP alts
        then doNumCon sc alts
        else map enumAlt alts

compileTerm (Lit _ lit) = pure $ CLit lit
compileTerm (Let _ nm t u) = do
  t' <- compileTerm t
  u' <- compileTerm u
  pure $ CLet nm t' u'
compileTerm (LetRec _ nm _ t u) = do
  t' <- compileTerm t
  u' <- compileTerm u
  pure $ CLetRec nm t' u'
compileTerm (Erased _) = pure CErased

compileFun : {{Ref2 Defs St}} → Tm -> M CExp
compileFun tm = go tm Lin
  where
    go : Tm -> SnocList String -> M CExp
    go (Lam _ nm _ _ t) acc = go t (acc :< nm)
    go tm Lin = compileTerm tm
    go tm args = CFun (args <>> Nil) <$> compileTerm tm

-- What are the Defs used for above? (Arity for name)
compileDCon : QName → ConInfo → Int → CExp
compileDCon (QN _ nm) EnumCon 0 = CLit $ LString nm
compileDCon (QN _ nm) info 0 = CConstr nm Nil
compileDCon (QN _ nm) info arity =
  let args = map (\k => "h\{show k}") (range 0 arity) in
  CFun args $ CConstr nm $ map (\k => CBnd $ arity - k - 1) (range 0 arity)

-- probably want to drop the Ref2 when we can
defToCExp : {{Ref2 Defs St}} → (QName × Def) -> M (QName × CExp)
defToCExp (qn, Axiom)          = pure $ (qn, CErased)
defToCExp (qn, DCon info arity _)   = pure $ (qn, compileDCon qn info arity)
defToCExp (qn, TCon arity _)   = pure $ (qn, compileDCon qn NormalCon arity)
defToCExp (qn, PrimTCon arity) = pure $ (qn, compileDCon qn NormalCon arity)
defToCExp (qn, PrimFn src _ deps) = pure $ (qn, CRaw src deps)
defToCExp (qn, Fn tm)          = (_,_ qn) <$> compileFun tm
