// wap types to Wax types.
//
// The one interesting decision here is that `u32` and `i32` are the same wasm
// type and differ only in the `Signage` they hand to an operator. Everything
// else is bookkeeping: which wap types are nominal in Wax (records, arrays,
// function types) and which are transparent (enumerations, subranges, sets, and
// aliases to scalars).

///|
/// The declaration a type name refers to.
fn Lowering::resolve(self : Lowering, name : String) -> @wap.TypeDef? {
  self.types.get(self.qualify(name))
}

///|
/// True when the name becomes a Wax type declaration of its own, rather than
/// disappearing into the scalar it stands for.
fn Lowering::nominal(self : Lowering, name : String) -> Bool {
  match self.resolve(name) {
    Some(Record(..)) => true
    Some(Alias(ArrayOf(_))) => true
    Some(Alias(Func(..))) => true
    _ => false
  }
}

///|
/// Follow aliases, enumerations, subranges and sets down to the type that
/// decides how an operator behaves.
fn Lowering::underlying(self : Lowering, t : @wap.Type) -> @wap.Type {
  match t {
    Named(n) =>
      match self.resolve(n) {
        Some(Enum(_)) => I32
        Some(Subrange(..)) => I32
        Some(SetOf(_)) => I32
        Some(Alias(inner)) =>
          if self.nominal(n) {
            t
          } else {
            self.underlying(inner)
          }
        _ => t
      }
    _ => t
  }
}

///|
/// Whether an integer type is signed, unsigned, or not an integer at all.
fn Lowering::signage(self : Lowering, t : @wap.Type?) -> @wasm_types.Signage? {
  match t {
    None => Some(Signed)
    Some(t) =>
      match self.underlying(t) {
        I8 | I16 | I32 | I64 => Some(Signed)
        U8 | U16 | U32 | U64 => Some(Unsigned)
        Bool | Char => Some(Unsigned)
        F32 | F64 => None
        _ => None
      }
  }
}

///|
/// A wap type as a Wax value type.
fn Lowering::valtype(
  self : Lowering,
  t : @wap.Type,
  span : @wap.Span,
) -> @wasm_types.ValType[@ast.Ident] {
  match t {
    I8 | I16 | I32 | U8 | U16 | U32 | Bool | Char => I32
    I64 | U64 => I64
    F32 => F32
    F64 => F64
    Any => Ref({ nullable: false, typ: Any, })
    Eq => Ref({ nullable: false, typ: Eq, })
    I31 => Ref({ nullable: false, typ: I31, })
    ArrayOf(e) =>
      Ref({
        nullable: false,
        typ: Type(self.ident(self.array_type(e, span), span)),
      })
    Func(params~, results~) =>
      Ref({
        nullable: false,
        typ: Type(self.ident(self.func_type(params, results, span), span)),
      })
    Named(n) =>
      if self.nominal(n) {
        Ref({ nullable: false, typ: Type(self.ident(self.mangle(n), span)), })
      } else {
        match self.resolve(n) {
          Some(_) => self.valtype(self.underlying(t), span)
          None => {
            self.error("`" + n + "` is not a type in this module", span)
            I32
          }
        }
      }
    Nullable(inner) => {
      let v = self.valtype(inner, span)
      match v {
        Ref(r) => Ref({ ..r, nullable: true, })
        _ => {
          self.error(
            "only a reference can be nullable",
            span,
            help="`?` applies to a record, an array, a function type or `any`",
          )
          v
        }
      }
    }
    Tuple(_) => {
      self.error(
        "a tuple cannot appear here",
        span,
        help="tuples exist on results and bindings only -- wasm has no tuple value, so storing one would mean allocating a record wap did not write down",
      )
      I32
    }
  }
}

///|
/// A wap type as the storage type of a record field or an array element.
///
/// This is where `u8` stops being an i32: in storage position it is a packed
/// byte, and every read of it therefore needs the zero-extending cast that Wax
/// makes the programmer write by hand.
fn Lowering::storage(
  self : Lowering,
  t : @wap.Type,
  span : @wap.Span,
) -> @wasm_types.StorageType[@ast.Ident] {
  match self.underlying(t) {
    I8 | U8 | Bool => Packed(I8)
    I16 | U16 => Packed(I16)
    _ => Value(self.valtype(t, span))
  }
}

///|
/// True when the type is stored packed, and so has to be widened on every read.
fn Lowering::is_packed(self : Lowering, t : @wap.Type) -> Bool {
  match self.underlying(t) {
    I8 | U8 | I16 | U16 | Bool => true
    _ => false
  }
}

///|
/// The element type of an array type, if it is one.
fn Lowering::elem_type(self : Lowering, t : @wap.Type) -> @wap.Type? {
  match t {
    ArrayOf(e) => Some(e)
    Nullable(inner) => self.elem_type(inner)
    Named(n) =>
      match self.resolve(n) {
        Some(Alias(inner)) => self.elem_type(inner)
        _ => None
      }
    _ => None
  }
}

///|
/// The record a type names, following aliases and `?`.
fn Lowering::record_name(self : Lowering, t : @wap.Type) -> String? {
  match t {
    Named(n) =>
      match self.resolve(n) {
        Some(Record(..)) => Some(self.qualify(n))
        Some(Alias(inner)) => self.record_name(inner)
        _ => None
      }
    Nullable(inner) => self.record_name(inner)
    _ => None
  }
}

///|
/// A field's type, searching the record's ancestors.
fn Lowering::field_type(
  self : Lowering,
  record : String,
  field : String,
) -> @wap.Type? {
  match self.resolve(record) {
    Some(Record(parent~, fields~)) => {
      for f in fields {
        if f.name == field {
          return Some(f.typ)
        }
      }
      match parent {
        Some(p) => self.field_type(p, field)
        None => None
      }
    }
    _ => None
  }
}

///|
/// Rewrite every name inside a type so it says which module owns it.
///
/// Doing this once, when the declaration is collected, is what keeps the rest
/// of the lowering from having to know which module a stored type came from.
fn Lowering::qualify_typ(self : Lowering, t : @wap.Type) -> @wap.Type {
  match t {
    Named(n) =>
      match builtin_or_self(n) {
        true => t
        false => Named(self.qualify(n))
      }
    Nullable(i) => Nullable(self.qualify_typ(i))
    ArrayOf(e) => ArrayOf(self.qualify_typ(e))
    Tuple(ts) => Tuple(ts.map(x => self.qualify_typ(x)))
    Func(params~, results~) =>
      Func(
        params=params.map(x => self.qualify_typ(x)),
        results=results.map(x => self.qualify_typ(x)),
      )
    _ => t
  }
}

///|
/// A name that is already qualified needs no help.
fn builtin_or_self(n : String) -> Bool {
  n.contains(".")
}

///|
/// The same, for a whole declaration.
fn Lowering::qualify_def(self : Lowering, d : @wap.TypeDef) -> @wap.TypeDef {
  match d {
    Record(parent~, fields~) =>
      Record(
        parent=match parent {
          Some(p) => Some(self.qualify(p))
          None => None
        },
        fields=fields.map(f => {
          (
            {
              name: f.name,
              mut_: f.mut_,
              typ: self.qualify_typ(f.typ),
              span: f.span,
            } : @wap.Field)
        }),
      )
    SetOf(e) => SetOf(self.qualify(e))
    Alias(t) => Alias(self.qualify_typ(t))
    _ => d
  }
}

///|
/// Every record that some other record extends, and so must stay open.
fn Lowering::parents(self : Lowering) -> Map[String, Unit] {
  let out : Map[String, Unit] = Map([])
  for _, def in self.types {
    if def is Record(parent=Some(p), ..) {
      out[p] = ()
    }
  }
  out
}

// ---------------------------------------------------- generated type names

///|
/// The Wax type name for an inline `[t]`, interning by structure so that two
/// occurrences of `[i32]` are the same wasm type.
fn Lowering::array_type(
  self : Lowering,
  elem : @wap.Type,
  span : @wap.Span,
) -> String {
  let key = "a:" + self.type_key(elem)
  match self.anon.get(key) {
    Some(n) => n
    None => {
      let name = self.mangle(
        "anon_array_" + (self.anon.length() + 1).to_string(),
      )
      self.anon[key] = name
      let at = self.loc(span)
      let st = self.storage(elem, span)
      self.extra_types.push({
        desc: (
          { name, loc: at, },
          {
            typ: Array({ mut_: true, typ: st, }),
            supertype: None,
            final_: true,
            descriptor: None,
            describes: None,
          },
        ),
        info: at,
      })
      name
    }
  }
}

///|
/// The Wax type name for an inline `fn(a) -> b`.
fn Lowering::func_type(
  self : Lowering,
  params : Array[@wap.Type],
  results : Array[@wap.Type],
  span : @wap.Span,
) -> String {
  let key = "f:" +
    params.map(t => self.type_key(t)).join(",") +
    "->" +
    results.map(t => self.type_key(t)).join(",")
  match self.anon.get(key) {
    Some(n) => n
    None => {
      let name = self.mangle("anon_fn_" + (self.anon.length() + 1).to_string())
      self.anon[key] = name
      let at = self.loc(span)
      let ps = []
      for t in params {
        ps.push(
          (
            { desc: (None, self.valtype(t, span)), info: at, } :
            @basic.Annotated[
              (@ast.Ident?, @wasm_types.ValType[@ast.Ident]),
              @basic.Location,
            ]),
        )
      }
      let rs = []
      for t in results {
        rs.push(self.valtype(t, span))
      }
      self.extra_types.push({
        desc: (
          { name, loc: at, },
          {
            typ: Func({ params: ps, results: rs, }),
            supertype: None,
            final_: true,
            descriptor: None,
            describes: None,
          },
        ),
        info: at,
      })
      name
    }
  }
}

///|
/// A structural key, so that equal types intern to one name.
fn Lowering::type_key(self : Lowering, t : @wap.Type) -> String {
  match t {
    I8 => "i8"
    I16 => "i16"
    I32 => "i32"
    I64 => "i64"
    U8 => "u8"
    U16 => "u16"
    U32 => "u32"
    U64 => "u64"
    F32 => "f32"
    F64 => "f64"
    Bool => "bool"
    Char => "char"
    Any => "any"
    Eq => "eq"
    I31 => "i31"
    Named(n) => "n:" + n
    Nullable(i) => "?" + self.type_key(i)
    ArrayOf(e) => "[" + self.type_key(e) + "]"
    Tuple(ts) => "(" + ts.map(t => self.type_key(t)).join(",") + ")"
    Func(params~, results~) =>
      "f(" +
      params.map(t => self.type_key(t)).join(",") +
      "->" +
      results.map(t => self.type_key(t)).join(",") +
      ")"
  }
}