{-# OPTIONS -v refl:20 #-}
open import 1Lab.Reflection.Signature
open import 1Lab.Reflection.HLevel
open import 1Lab.Reflection.Subst
open import 1Lab.HLevel.Closure
open import 1Lab.Reflection
open import 1Lab.Equiv
open import 1Lab.Path
open import 1Lab.Type

open import Data.Nat.Base

open import Meta.Foldable
open import Meta.Append

import Prim.Data.Sigma as S
import Prim.Data.Nat as N

module 1Lab.Reflection.Record where

Fields : Type
Fields = List (Name × List Name)

-- Annotate a list of field names with a "projection path" to access
-- them in the corresponding Σ-type.
-- Example: [f1, f2, f3] ↦ [(f1, .fst), (f2, .snd .fst), (f3, .snd .snd)]
field-names→paths : List (Arg Name)  Fields
field-names→paths [] = []
field-names→paths (arg _ nm  []) = (nm , [])  []
field-names→paths (arg _ x   (y  ys)) with field-names→paths (y  ys)
... | fields = (x , quote fst  [])  map  (f , p)  f , quote snd  p) fields

-- Given the name of a record type and the corresponding Σ-type, generate
-- the type of the corresponding iso helper.
-- Example: ∀ {xs : Δ} → Iso (R xs) (Σ A (Σ B C))
record→iso : Name  Term  TC Term
record→iso namen unfolded = go [] =<< normalise =<< get-type namen where
  go : List ArgInfo  Term  TC Term
  go acc (pi argu@(arg i argTy) (abs s ty)) = do
    -- If `go` ever needs the TC context to be correct, uncomment the
    -- following line.
    r  -- extend-context "arg" argu $
        go (i  acc) ty
    pure $ pi (argH argTy) (abs s r)

  go acc (agda-sort _) = do
    let rec = def namen (reverse (map-up  n i  arg i (var n [])) 0 acc))
    pure $ def (quote Iso) (rec v∷ unfolded v∷ [])

  go _ _ = typeError [ "Unsupported record type: " , nameErr namen ]

-- Given the name of a record type, generate the type of the
-- corresponding path constructor, with paths in propositions omitted.
-- Example: ∀ {xs : Δ} {r1 r2 : R xs} p1 p2 p3 → r1 ≡ r2
-- The second output is a list of booleans indicating which fields are
-- propositions.
record→path : Name  List (Arg Name)  Term  Term  TC (Term × List Bool)
record→path namen fields `R-ty con-ty = go [] `R-ty con-ty where
  go' : Nat  Term  TC (Term × List Bool)
  go' m (pi argu@(arg i fTy) (abs s ty)) = do
    prop? 
      (do
        `m  new-meta (def (quote H-Level) (fTy v∷ lit (nat 1) v∷ []))
        unify `m $ def (quote auto) []
        pure true)
      <|> pure false
    extend-context "" argu $ case prop? of λ where
      false  do
        r , ps  go' (suc m) ty
        pure $ pi (argN unknown) (abs "p" r) , true  ps
      true  do
        r , ps  go' m ty
        pure $ r , false  ps
  go' m (def _ _) = do
    pure (def (quote _≡_) (var (suc m) [] v∷ var m [] v∷ []) , [])
  go' _ _ = typeError [ "Unsupported record type: " , nameErr namen ]

  go : List ArgInfo  Term  Term  TC (Term × List Bool)
  go acc (pi argu@(arg i argTy) (abs s ty)) (pi _ (abs _ con-ty)) = do
    r , ps  extend-context "arg" argu $ go (i  acc) ty con-ty
    pure $ pi (argH argTy) (abs s r) , ps

  go acc (agda-sort _) con-ty = do
    ty , ps  go' 0 con-ty
    let recTy = def namen (reverse (map-up  n i  arg i (var n [])) 0 acc))
        ty = pi (argH recTy) $ abs "r1"
           $ pi (argH (raise 1 recTy)) $ abs "r2"
           $ ty
    pure (ty , ps)

  go _ _ _ = typeError [ "Unsupported record type: " , nameErr namen ]

-- Generate the clauses of the isomorphism corresponding to the given field.
undo-clause : Name × List Name  Clause
undo-clause (r-field , sel-path) = clause
  (("sig" , argN unknown)  [])
  [ argN (proj (quote snd))
  , argN (proj (quote is-iso.from))
  , argN (var 0)
  , argN (proj r-field)
  ]
  (foldr  n t  def n (t v∷ [])) (var 0 []) (reverse sel-path))

redo-clause : Name × List Name  Clause
redo-clause (r-field , sel-path) = clause
  (("rec" , argN unknown)  [])
  (argN (proj (quote fst))  argN (var 0)  map (argN  proj) sel-path)
  (def r-field (var 0 [] v∷ []))

undo-redo-clause : Name × List Name  Clause
undo-redo-clause ((r-field , _)) = clause
  (("sig" , argN unknown)  ("i" , argN (quoteTerm I))  [])
  ( argN (proj (quote snd))  argN (proj (quote is-iso.linv))
   argN (var 1)  argN (var 0)  argN (proj r-field)  [])
  (def r-field (var 1 [] v∷ []))

redo-undo-clause : Name × List Name  Clause
redo-undo-clause (r-field , sel-path) = clause
  (("rec" , argN unknown)  ("i" , argN (quoteTerm I))  [])
  (  [ argN (proj (quote snd)) , argN (proj (quote is-iso.rinv)) , argN (var 1) , argN (var 0) ]
  <> map (argN  proj) sel-path)
  (foldr  n t  def n (t v∷ [])) (var 1 []) (reverse sel-path))

-- Transform the type of a record constructor into a Σ-type isomorphic
-- to the record.
pi-term→sigma : Term  TC Term
pi-term→sigma (pi (arg _ x) (abs n (def n' _))) = pure x
pi-term→sigma (pi (arg _ x) (abs n y)) = do
  sig  pi-term→sigma y
  pure $ def (quote S.Σ) (x v∷ lam visible (abs n sig) v∷ [])
pi-term→sigma _ = typeError (strErr "Not a record type constructor! "  [])

instantiate' : Term  Term  Term
instantiate' (pi _ (abs _ xs)) (pi _ (abs _ b)) = instantiate' xs b
instantiate' (agda-sort _) tm = tm
instantiate' _ tm = tm

-- [true, false, true] ↦ [var 1, prop!, var 0]
bools→terms : Nat  List Bool  List Term
bools→terms zero [] = []
bools→terms (suc n) (true  l) = var n []  bools→terms n l
bools→terms n (false  l) = def (quote prop!) []  bools→terms n l
bools→terms _ _ = []

-- [a, b, c] ↦ Σ-pathp a (Σ-pathp b c)
Σ-pathpⁿ : List Term  Term
Σ-pathpⁿ [] = unknown -- empty record types are not supported
Σ-pathpⁿ (p  []) = p
Σ-pathpⁿ (p  p'  ps) = def (quote Σ-pathp) (p v∷ Σ-pathpⁿ (p'  ps) v∷ [])

make-record-iso-sigma : Bool  Name  Name  TC 
make-record-iso-sigma declare? nm `R = do
  `R-con , fields  get-record-type `R

  let fields = field-names→paths fields

  when declare? do
    `R-ty  normalise =<< get-type `R
    con-ty  normalise =<< get-type `R-con
    let con-ty = instantiate' `R-ty con-ty
    unfolded  pi-term→sigma con-ty
    ty  record→iso `R unfolded
    declare (argN nm) ty

  define-function nm
    ( map redo-clause fields ++
      map undo-clause fields ++
      map redo-undo-clause fields ++
      map undo-redo-clause fields)

{-
Usage: slap

  unquoteDecl eqv = declare-record-iso eqv (quote Your-record)

in a module! The macro *DOES NOT WORK* in where clauses (or in record
definitions!). It does work in arbitrarily nested modules, and in the
top-level module, as the example below demonstrates: we can declare the
isomorphism adjacent to the record, outside the module where the record
is defined, and in a different module altogether.

All features of non-recursive records are supported, including
no-eta-equality, implicit and instance fields, and fields with implicit
arguments as types.

You can also use

  unquoteDecl R-path = declare-record-path R-path (quote R)

to define a "path constructor" helper for building a path between two
elements of R. Defining PathPs over paths in the record's parameters is
not (yet) supported.
-}

declare-record-iso : Name  Name  TC 
declare-record-iso = make-record-iso-sigma true

define-record-iso : Name  Name  TC 
define-record-iso = make-record-iso-sigma false

make-record-path : Bool  Name  Name  TC 
make-record-path declare? nm `R = do
  `R-con , fields  get-record-type `R
  eqv  helper-function-name nm "eqv"
  declare-record-iso eqv `R

  `R-ty  normalise =<< get-type `R
  con-ty  normalise =<< get-type `R-con
  ty , ps  record→path `R fields `R-ty con-ty
  let ps' = filter id ps
      n = length ps'

  when declare? do
    declare (argN nm) ty

  define-function nm
    [ clause
      (map  _  "p" , argN unknown) ps')
      (map-up  i _  argN (var (n - i))) 1 ps')
      (def (quote Iso.injective) (def eqv [] v∷ Σ-pathpⁿ (bools→terms n ps) v∷ []))
    ]

declare-record-path : Name  Name  TC 
declare-record-path = make-record-path true

define-record-path : Name  Name  TC 
define-record-path = make-record-path false

{-
Metaprogram for defining instances of H-Level (R x) n, where R x is a
record type whose components can all immediately be seen to have h-level
n.

That is, this works for things like Cat.Morphism._↪_, since the H-Level
automation already works for showing that its representation as a Σ-type
has hlevel 2, but it does not work for Algebra.Group.is-group, since
that requires specific knowledge about is-group to work.

Can be used either for unquoteDecl or unquoteDef. In the latter case, it
is possible to give the generated instance a more specific context which
might help to automatically derive instances for more types.
-}

private
  record-hlevel-instance
    :  { ℓ'} {A : Type } {B : Type ℓ'} (n : Nat)  _ : H-Level A n 
     Iso B A
      {k}  p : n  k 
     H-Level B k
  record-hlevel-instance n im  p  = hlevel-instance $
    Iso→is-hlevel _ im (is-hlevel-le _ _ p (hlevel _))

declare-record-hlevel : (n : Nat)  Name  Name  TC 
declare-record-hlevel lvl inst rec = do
  (rec-tele , _)  pi-view <$> get-type rec

  eqv  helper-function-name rec "isom"
  declare-record-iso eqv rec

  let
    args    = reverse $ map-up  n (_ , arg i _)  arg i (var₀ n)) 2 (reverse rec-tele)

    head-ty = it H-Level ##ₙ def rec args ##ₙ var₀ 1

    inst-ty = unpi-view (map  (nm , arg _ ty)  nm , argH ty) rec-tele) $
      pi (argH (it Nat)) $ abs "n" $
      pi (argI (it _≤_ ##ₙ lit (nat lvl) ##ₙ var₀ 0)) $ abs "le" $
      head-ty

  declare (argI inst) inst-ty
  define-function inst
    [ clause [] [] (it record-hlevel-instance ##ₙ lit (nat lvl) ##ₙ def₀ eqv) ]

private
  module _ {} (A : Type ) where
    record T : Type  where
      no-eta-equality
      field
         fp  : A
        {f} : A  A
        fixed : f fp  fp

    unquoteDecl eqv = declare-record-iso eqv (quote T)

    _ : Iso T (S.Σ A  fp  S.Σ (A  A)  f  f fp  fp)))
    _ = eqv

  unquoteDecl eqv-outside = declare-record-iso eqv-outside (quote T)

  _ : { : Level} {A : Type }  Iso (T A) (S.Σ A  fp  S.Σ (A  A)  f  f fp  fp)))
  _ = eqv-outside

  unquoteDecl T-path = declare-record-path T-path (quote T)

  _ : { : Level} {A : Type } {t1 t2 : T A}   a b c  t1  t2
  _ = T-path

  module _ (x : Nat) where
    unquoteDecl eqv-extra = declare-record-iso eqv-extra (quote T)

  _ : Nat  { : Level} {A : Type }
     Iso (T A) (S.Σ A  fp  S.Σ (A  A)  f  f fp  fp)))
  _ = eqv-extra

  record T2 : Type where
    -- works without eta equality too
    field
      some-field : Nat

  s-eqv : Iso T2 Nat
  unquoteDef s-eqv = define-record-iso s-eqv (quote T2)