module Idris.Elab.Record(elabRecord) where
import Idris.AbsSyntax
import Idris.Docstrings
import Idris.Error
import Idris.Delaborate
import Idris.Imports
import Idris.Elab.Term
import Idris.Coverage
import Idris.DataOpts
import Idris.Providers
import Idris.Primitives
import Idris.Inliner
import Idris.PartialEval
import Idris.DeepSeq
import Idris.Output (iputStrLn, pshow, iWarn, sendHighlighting)
import IRTS.Lang
import Idris.ParseExpr (tryFullExpr)
import Idris.Elab.Type
import Idris.Elab.Data
import Idris.Elab.Utils
import Idris.Core.TT
import Idris.Core.Evaluate
import Idris.Elab.Data
import Data.Maybe
import Data.List
import Control.Monad
elabRecord :: ElabInfo
           -> ElabWhat
           -> (Docstring (Either Err PTerm)) 
           -> SyntaxInfo -> FC -> DataOpts
           -> Name  
           -> FC 
           -> [(Name, FC, Plicity, PTerm)] 
           -> [(Name, Docstring (Either Err PTerm))] 
           -> [(Maybe (Name, FC), Plicity, PTerm, Maybe (Docstring (Either Err PTerm)))] 
           -> Maybe (Name, FC) 
           -> (Docstring (Either Err PTerm)) 
           -> SyntaxInfo 
           -> Idris ()
elabRecord info what doc rsyn fc opts tyn nfc params paramDocs fields cname cdoc csyn
  = do logLvl 1 $ "Building data declaration for " ++ show tyn
       
       let tycon = generateTyConType params
       logLvl 1 $ "Type constructor " ++ showTmImpls tycon
       
       
       dconName <- generateDConName (fmap fst cname)
       let dconTy = generateDConType params fieldsWithNameAndDoc
       logLvl 1 $ "Data constructor: " ++ showTmImpls dconTy
       
       logLvl 1 $ foldr (++) "" $ intersperse "\n" (map show dconsArgDocs)
       let datadecl = case what of
                           ETypes -> PLaterdecl tyn NoFC tycon
                           _ -> PDatadecl tyn NoFC tycon [(cdoc, dconsArgDocs, dconName, NoFC, dconTy, fc, [])]
       elabData info rsyn doc paramDocs fc opts datadecl
       
       let parameters = [(n,pt) | (n, _, _, pt) <- params]
       let projections = [n | (n, _, _, _, _) <- fieldsWithName]
       addRecord tyn (RI parameters dconName projections)
       addIBC (IBCRecord tyn)
       when (what /= ETypes) $ do
           logLvl 1 $ "fieldsWithName " ++ show fieldsWithName
           logLvl 1 $ "fieldsWIthNameAndDoc " ++ show fieldsWithNameAndDoc
           elabRecordFunctions info rsyn fc tyn paramsAndDoc fieldsWithNameAndDoc dconName target
           sendHighlighting $
             [(nfc, AnnName tyn Nothing Nothing Nothing)] ++
             maybe [] (\(_, cnfc) -> [(cnfc, AnnName dconName Nothing Nothing Nothing)]) cname ++
             [(ffc, AnnBoundName fn False) | (fn, ffc, _, _, _) <- fieldsWithName]
  where
    
    generateTyConType :: [(Name, FC, Plicity, PTerm)] -> PTerm
    generateTyConType ((n, nfc, p, t) : rest) = PPi p (nsroot n) nfc t (generateTyConType rest)
    generateTyConType [] = (PType fc)
    
    generateDConName :: Maybe Name -> Idris Name
    generateDConName (Just n) = return $ expandNS csyn n
    generateDConName Nothing  = uniqueName (expandNS csyn $ sMN 0 ("Mk" ++ (show (nsroot tyn))))
      where
        uniqueName :: Name -> Idris Name
        uniqueName n = do i <- getIState
                          case lookupTyNameExact n (tt_ctxt i) of
                            Just _  -> uniqueName (nextName n)
                            Nothing -> return n
    
    generateDConType :: [(Name, FC, Plicity, PTerm)] -> [(Name, FC, Plicity, PTerm, a)] -> PTerm
    generateDConType ((n, nfc, _, t) : ps) as                  = PPi impl (nsroot n) NoFC t (generateDConType ps as)
    generateDConType []               ((n, _, p, t, _) : as) = PPi p    (nsroot n) NoFC t (generateDConType [] as)
    generateDConType [] [] = target
    
    target :: PTerm
    target = PApp fc (PRef fc [] tyn) $ map (uncurry asPRefArg) [(p, n) | (n, _, p, _) <- params]
    paramsAndDoc :: [(Name, FC, Plicity, PTerm, Docstring (Either Err PTerm))]
    paramsAndDoc = pad params paramDocs
      where
        pad :: [(Name, FC, Plicity, PTerm)] -> [(Name, Docstring (Either Err PTerm))] -> [(Name, FC, Plicity, PTerm, Docstring (Either Err PTerm))]
        pad ((n, fc, p, t) : rest) docs
          = let d = case lookup n docs of
                     Just d' -> d
                     Nothing -> emptyDocstring
            in (n, fc, p, t, d) : (pad rest docs)
        pad _ _ = []
    dconsArgDocs :: [(Name, Docstring (Either Err PTerm))]
    dconsArgDocs = paramDocs ++ (dcad fieldsWithName)
      where
        dcad :: [(Name, FC, Plicity, PTerm, Maybe (Docstring (Either Err PTerm)))] -> [(Name, Docstring (Either Err PTerm))]
        dcad ((n, _, _, _, (Just d)) : rest) = ((nsroot n), d) : (dcad rest)
        dcad (_ : rest) = dcad rest
        dcad [] = []
    fieldsWithName :: [(Name, FC, Plicity, PTerm, Maybe (Docstring (Either Err PTerm)))]
    fieldsWithName = fwn [] fields
      where
        fwn :: [Name] -> [(Maybe (Name, FC), Plicity, PTerm, Maybe (Docstring (Either Err PTerm)))] -> [(Name, FC, Plicity, PTerm, Maybe (Docstring (Either Err PTerm)))]
        fwn ns ((n, p, t, d) : rest)
          = let nn = case n of
                      Just n' -> n'
                      Nothing -> newName ns baseName
                withNS = expandNS rsyn (fst nn)
            in (withNS, snd nn, p, t, d) : (fwn (fst nn : ns) rest)
        fwn _ _ = []
        baseName = (sUN "__pi_arg", NoFC)
        newName :: [Name] -> (Name, FC) -> (Name, FC)
        newName ns (n, nfc)
          | n `elem` ns = newName ns (nextName n, nfc)
          | otherwise = (n, nfc)
    fieldsWithNameAndDoc :: [(Name, FC, Plicity, PTerm, Docstring (Either Err PTerm))]
    fieldsWithNameAndDoc = fwnad fieldsWithName
      where
        fwnad :: [(Name, FC, Plicity, PTerm, Maybe (Docstring (Either Err PTerm)))] -> [(Name, FC, Plicity, PTerm, Docstring (Either Err PTerm))]
        fwnad ((n, nfc, p, t, d) : rest)
          = let doc = fromMaybe emptyDocstring d
            in (n, nfc, p, t, doc) : (fwnad rest)
        fwnad [] = []
elabRecordFunctions :: ElabInfo -> SyntaxInfo -> FC
                    -> Name 
                    -> [(Name, FC, Plicity, PTerm, Docstring (Either Err PTerm))] 
                    -> [(Name, FC, Plicity, PTerm, Docstring (Either Err PTerm))] 
                    -> Name 
                    -> PTerm 
                    -> Idris ()
elabRecordFunctions info rsyn fc tyn params fields dconName target
  = do logLvl 1 $ "Elaborating helper functions for record " ++ show tyn
       logLvl 1 $ "Fields: " ++ show fieldNames
       logLvl 1 $ "Params: " ++ show paramNames
       
       i <- getIState
       ttConsTy <-
         case lookupTyExact dconName (tt_ctxt i) of
               Just as -> return as
               Nothing -> tclift $ tfail $ At fc (Elaborating "record " tyn (InternalMsg "It seems like the constructor for this record has disappeared. :( \n This is a bug. Please report."))
       
       let constructorArgs = getArgTys ttConsTy
       logLvl 1 $ "Cons args: " ++ show constructorArgs
       logLvl 1 $ "Free fields: " ++ show (filter (not . isFieldOrParam') constructorArgs)
       
       let freeFieldsForElab = map (freeField i) (filter (not . isFieldOrParam') constructorArgs)
           
       
       
       let paramsForElab = [((nsroot n), (paramName n), impl, t, d) | (n, _, _, t, d) <- params] 
       
       let userFieldsForElab = [((nsroot n), n, p, t, d) | (n, nfc, p, t, d) <- fields]
       
       let projectors = [(n, n', p, t, d, i) | ((n, n', p, t, d), i) <- zip (freeFieldsForElab ++ paramsForElab ++ userFieldsForElab) [0..]]       
       
       elabProj dconName projectors
       logLvl 1 $ "Dependencies: " ++ show fieldDependencies
       logLvl 1 $ "Depended on: " ++ show dependedOn
       
       let updaters = [(n, n', p, t, d, i) | ((n, n', p, t, d), i) <- zip (paramsForElab ++ userFieldsForElab) [0..]]
       
       elabUp dconName updaters
  where
    
    placeholderArg :: Plicity -> Name -> PArg
    placeholderArg p n = asArg p (nsroot n) Placeholder
    
    fieldNames :: [Name]
    fieldNames = [nsroot n | (n, _, _, _, _) <- fields]
    paramNames :: [Name]
    paramNames = [nsroot n | (n, _, _, _, _) <- params]
    isFieldOrParam :: Name -> Bool
    isFieldOrParam n = n `elem` (fieldNames ++ paramNames)
    isFieldOrParam' :: (Name, a) -> Bool
    isFieldOrParam' = isFieldOrParam . fst
    isField :: Name -> Bool
    isField = flip elem fieldNames
    isField' :: (Name, a, b, c, d, f) -> Bool
    isField' (n, _, _, _, _, _) = isField n
    fieldTerms :: [PTerm]
    fieldTerms = [t | (_, _, _, t, _) <- fields]
    
    
    
    
    freeField :: IState -> (Name, TT Name) -> (Name, Name, Plicity, PTerm, Docstring (Either Err PTerm))
    freeField i arg = let nameInCons = fst arg 
                          nameFree = expandNS rsyn (freeName $ fst arg) 
                          plicity = impl 
                          fieldType = delab i (snd arg) 
                          doc = emptyDocstring 
                      in (nameInCons, nameFree, plicity, fieldType, doc) 
    freeName :: Name -> Name
    freeName (UN n) = sUN ("free_" ++ str n)
    freeName (MN i n) = sMN i ("free_" ++ str n)
    freeName (NS n s) = NS (freeName n) s
    freeName n = n
    
    zipParams :: IState -> [(Name, Plicity, PTerm)] -> [(Name, Docstring (Either Err PTerm))] -> [(Name, PTerm, Docstring (Either Err PTerm))]
    zipParams i ((n, _, t) : rest) ((_, d) : rest') = (n, t, d) : (zipParams i rest rest')
    zipParams i ((n, _, t) : rest) [] = (n, t, emptyDoc) : (zipParams i rest [])
      where emptyDoc = annotCode (tryFullExpr rsyn i) emptyDocstring
    zipParams _ [] [] = []
    paramName :: Name -> Name
    paramName (UN n) = sUN ("param_" ++ str n)
    paramName (MN i n) = sMN i ("param_" ++ str n)
    paramName (NS n s) = NS (paramName n) s
    paramName n = n
    
    elabProj :: Name -> [(Name, Name, Plicity, PTerm, Docstring (Either Err PTerm), Int)] -> Idris ()
    elabProj cn fs = let phArgs = map (uncurry placeholderArg) [(p, n) | (n, _, p, _, _, _) <- fs]
                         elab = \(n, n', p, t, doc, i) ->
                              
                           do let t' = projectInType [(m, m') | (m, m', _, _, _, _) <- fs
                                                              
                                                              , not (m `elem` paramNames)] t
                              elabProjection info n n' p t' doc rsyn fc target cn phArgs fieldNames i
                     in mapM_ elab fs
    
    elabUp :: Name -> [(Name, Name, Plicity, PTerm, Docstring (Either Err PTerm), Int)] -> Idris ()
    elabUp cn fs = let args = map (uncurry asPRefArg) [(p, n) | (n, _, p, _, _, _) <- fs]
                       elab = \(n, n', p, t, doc, i) -> elabUpdate info n n' p t doc rsyn fc target cn args fieldNames i (optionalSetter n)
                   in mapM_ elab fs
    
    optionalSetter :: Name -> Bool
    optionalSetter n = n `elem` dependedOn
        
    
    fieldDependencies :: [(Name, [Name])]
    fieldDependencies = map (uncurry fieldDep) [(n, t) | (n, _, _, t, _) <- fields ++ params]
      where
        fieldDep :: Name -> PTerm -> (Name, [Name])
        fieldDep n t = ((nsroot n), paramNames ++ fieldNames `intersect` allNamesIn t)
    
    dependentFields :: [Name]
    dependentFields = filter depends fieldNames
      where
        depends :: Name -> Bool
        depends n = case lookup n fieldDependencies of
                      Just xs -> not $ null xs
                      Nothing -> False
    
    dependedOn :: [Name]
    dependedOn = concat ((catMaybes (map (\x -> lookup x fieldDependencies) fieldNames)))
elabProjection :: ElabInfo
               -> Name 
               -> Name 
               -> Plicity 
               -> PTerm 
               -> (Docstring (Either Err PTerm)) 
               -> SyntaxInfo 
               -> FC -> PTerm 
               -> Name 
               -> [PArg] 
               -> [Name] 
               -> Int 
               -> Idris ()
elabProjection info cname pname plicity projTy pdoc psyn fc targetTy cn phArgs fnames index
  = do logLvl 1 $ "Generating Projection for " ++ show pname
       
       let ty = generateTy
       logLvl 1 $ "Type of " ++ show pname ++ ": " ++ show ty
       
       let lhs = generateLhs
       logLvl 1 $ "LHS of " ++ show pname ++ ": " ++ showTmImpls lhs
       let rhs = generateRhs
       logLvl 1 $ "RHS of " ++ show pname ++ ": " ++ showTmImpls rhs
       rec_elabDecl info EAll info ty
       let clause = PClause fc pname lhs [] rhs []
       rec_elabDecl info EAll info $ PClauses fc [] pname [clause]
  where
    
    generateTy :: PDecl
    generateTy = PTy pdoc [] psyn fc [] pname NoFC $
                   PPi expl recName NoFC targetTy projTy
    
    generateLhs :: PTerm
    generateLhs = let args = lhsArgs index phArgs
                  in PApp fc (PRef fc [] pname) [pexp (PApp fc (PRef fc [] cn) args)]
      where
        lhsArgs :: Int -> [PArg] -> [PArg]
        lhsArgs 0 (_ : rest) = (asArg plicity (nsroot cname) (PRef fc [] pname_in)) : rest
        lhsArgs i (x : rest) = x : (lhsArgs (i1) rest)
        lhsArgs _ [] = []
    
    pname_in :: Name
    pname_in = rootname 
    rootname :: Name
    rootname = nsroot cname
    
    generateRhs :: PTerm
    generateRhs = PRef fc [] pname_in
elabUpdate :: ElabInfo
           -> Name 
           -> Name 
           -> Plicity 
           -> PTerm 
           -> (Docstring (Either Err PTerm)) 
           -> SyntaxInfo 
           -> FC -> PTerm 
           -> Name 
           -> [PArg] 
           -> [Name] 
           -> Int 
           -> Bool 
           -> Idris ()
elabUpdate info cname pname plicity pty pdoc psyn fc sty cn args fnames i optional
  = do logLvl 1 $ "Generating Update for " ++ show pname
       
       let ty = generateTy
       logLvl 1 $ "Type of " ++ show set_pname ++ ": " ++ show ty
       
       let lhs = generateLhs
       logLvl 1 $ "LHS of " ++ show set_pname ++ ": " ++ showTmImpls lhs
       
       let rhs = generateRhs
       logLvl 1 $ "RHS of " ++ show set_pname ++ ": " ++ showTmImpls rhs
       let clause = PClause fc set_pname lhs [] rhs []       
       idrisCatch (do rec_elabDecl info EAll info ty
                      rec_elabDecl info EAll info $ PClauses fc [] set_pname [clause])
         (\err -> logLvl 1 $ "Could not generate update function for " ++ show pname)
                  
  where
    
    generateTy :: PDecl
    generateTy = PTy pdoc [] psyn fc [] set_pname NoFC $
                   PPi expl (nsroot pname) NoFC pty $
                     PPi expl recName NoFC sty (substInput sty)
      where substInput = substMatches [(cname, PRef fc [] (nsroot pname))]
    
    set_pname :: Name
    set_pname = set_name pname
    set_name :: Name -> Name
    set_name (UN n) = sUN ("set_" ++ str n)
    set_name (MN i n) = sMN i ("set_" ++ str n)
    set_name (NS n s) = NS (set_name n) s
    set_name n = n
    
    generateLhs :: PTerm
    generateLhs = PApp fc (PRef fc [] set_pname) [pexp $ PRef fc [] pname_in, pexp constructorPattern]
      where
        constructorPattern :: PTerm
        constructorPattern = PApp fc (PRef fc [] cn) args
    
    pname_in :: Name
    pname_in = in_name rootname
    rootname :: Name
    rootname = nsroot pname
    
    generateRhs :: PTerm
    generateRhs = PApp fc (PRef fc [] cn) (newArgs i args)
      where
        newArgs :: Int -> [PArg] -> [PArg]
        newArgs 0 (_ : rest) = (asArg plicity (nsroot cname) (PRef fc [] pname_in)) : rest
        newArgs i (x : rest) = x : (newArgs (i1) rest)
        newArgs _ [] = []
in_name :: Name -> Name
in_name (UN n) = sMN 0 (str n ++ "_in")
in_name (MN i n) = sMN i (str n ++ "_in")
in_name (NS n s) = NS (in_name n) s
in_name n = n
asArg :: Plicity -> Name -> PTerm -> PArg
asArg (Imp os _ _ _) n t = PImp 0 False os n t
asArg (Exp os _ _) n t = PExp 0 os n t
asArg (Constraint os _) n t = PConstraint 0 os n t
asArg (TacImp os _ s) n t = PTacImplicit 0 os n s t
recName :: Name
recName = sMN 0 "rec"
recRef = PRef emptyFC [] recName
projectInType :: [(Name, Name)] -> PTerm -> PTerm
projectInType xs = mapPT st
  where
    st :: PTerm -> PTerm
    st (PRef fc hls n)
      | Just pn <- lookup n xs = PApp fc (PRef fc hls pn) [pexp recRef]
    st t = t
asPRefArg :: Plicity -> Name -> PArg
asPRefArg p n = asArg p (nsroot n) $ PRef emptyFC [] (nsroot n)