///|
pub struct Module {
  context : Context
  functions : Map[String, Function]
  globals : Map[String, &GlobalValue]
  global_strings : Map[String, GlobalConstant]
  mut globalstr_count : Int
  srcFileName : String
  moduleID : String
  dataLayout : DataLayout
}

///|
pub fn Module::new(
  moduleID : String,
  srcFileName : String,
  ctx : Context,
) -> Module {
  Module::{
    context: ctx,
    functions: Map::new(),
    globals: Map::new(),
    global_strings: Map::new(),
    globalstr_count: 0,
    srcFileName,
    moduleID,
    dataLayout: DataLayout::new(Little),
  }
}

///|
pub fn Module::getFunction(self : Module, name : String) -> Function? {
  match self.globals.get(name) {
    Some(f) =>
      match f.asGlobalValueEnum() {
        Function(func) => Some(func)
        _ => None
      }
    None => None
  }
}

///|
pub fn Module::getDataLayout(self : Module) -> DataLayout {
  self.dataLayout
}

///|
pub fn Module::addFunction(
  self : Module,
  fty : FunctionType,
  name : String,
  linkage? : Linkage = External,
  visibility? : Visibility = Default,
  unnamed_addr? : UnnamedAddr = NoUnnamedAddr,
  addressSpace? : AddressSpace = AddressSpace(0),
) -> Function raise LLVMValueError {
  if name.is_empty() {
    let msg = "Misuse `Module::addFunction`: function name cannot be empty."
    raise LLVMValueError(msg)
  }
  guard !isInValidName(name) else {
    let msg = "Misuse `Module::addFunction`: function name '\{name}' is invalid."
    raise LLVMValueError(msg)
  }
  guard !self.globals.contains(name) else {
    let msg = "Misuse `Module::addFunction`: global symbol '\{name}' already exists in the module."
    raise LLVMValueError(msg)
  }
  let index = self.functions.length()
  let f = Function::new(
    fty,
    name,
    linkage~,
    visibility~,
    unnamed_addr~,
    addressSpace~,
    index,
    self,
  )
  self.globals.set(name, f)
  self.functions.set(name, f)
  f
}

///|
pub fn Module::addGlobalVariable(
  self : Self,
  ty : &Type,
  name : String,
  initializer? : &Constant,
  linkage? : Linkage = External,
  visibility? : Visibility = Default,
  unnamed_addr? : UnnamedAddr = NoUnnamedAddr,
) -> GlobalVariable raise LLVMValueError {
  if name.is_empty() {
    let msg = "Misuse `Module::addGlobalVariable`: global variable name cannot be empty."
    raise LLVMValueError(msg)
  }
  guard !isInValidName(name) else {
    let msg = "Misuse `Module::addGlobalVariable`: global variable name '\{name}' is invalid."
    raise LLVMValueError(msg)
  }
  guard !self.globals.contains(name) else {
    let msg = "Misuse `Module::addGlobalVariable`: global symbol '\{name}' already exists in the module."
    raise LLVMValueError(msg)
  }
  let g = GlobalVariable::new(
    ty,
    name,
    linkage~,
    visibility~,
    unnamed_addr~,
    initializer~,
    self,
  )
  self.globals.set(name, g)
  g
}

///|
pub fn Module::addGlobalConstant(
  self : Self,
  ty : &Type,
  name : String,
  value : &Constant,
  linkage? : Linkage = External,
  visibility? : Visibility = Default,
  unnamed_addr? : UnnamedAddr = NoUnnamedAddr,
) -> GlobalConstant raise LLVMValueError {
  if name.is_empty() {
    let msg = "Misuse `Module::addGlobalConstant`: global constant name cannot be empty."
    raise LLVMValueError(msg)
  }
  guard !isInValidName(name) else {
    let msg = "Misuse `Module::addGlobalConstant`: global constant name '\{name}' is invalid."
    raise LLVMValueError(msg)
  }
  guard !self.globals.contains(name) else {
    let msg = "Misuse `Module::addGlobalConstant`: global symbol '\{name}' already exists in the module."
    raise LLVMValueError(msg)
  }
  let g = GlobalConstant::new(
    ty,
    name,
    value,
    self,
    linkage~,
    visibility~,
    unnamed_addr~,
  )
  self.globals.set(name, g)
  g
}

///|
pub fn Module::addGlobalString(
  self : Self,
  string : String,
  name? : String = "",
) -> GlobalConstant raise {
  if self.global_strings.contains(string) {
    return self.global_strings[string]
  }
  let ctx = self.getContext()
  let const_str = ConstantString::new(ctx, string)
  let ty = const_str.getType()
  let name = match name {
    "" => {
      self.globalstr_count += 1
      "gstr" + self.globalstr_count.to_string()
    }
    n => n
  }
  let gvar = self.addGlobalConstant(
    ty,
    name,
    const_str,
    linkage=Private,
    unnamed_addr=Global,
  )
  gvar
}

///|
pub fn Module::getContext(self : Module) -> Context {
  self.context
}

///|
pub impl Show for Module with output(self, logger) {
  logger.write_string(";; ModuleID = '\{self.moduleID}'\n")
  logger.write_string(";; Source File = \"\{self.srcFileName}\"\n")
  logger.write_string(";; Generated by MoonLLVM, not llvm\n\n")
  let global_var_and_consts = self.globals
    .values()
    .filter(gv => match gv.asGlobalValueEnum() {
      GlobalVariable(_) => true
      GlobalConstant(_) => true
      _ => false
    })
    .collect()
  global_var_and_consts.each(gv => logger.write_string("\{gv}\n"))
  let { namedStructTypes, .. } = self.context.pimpl.unwrap()
  for _, st in namedStructTypes {
    logger.write_string(st.full_info() + "\n\n")
  }
  let func_str = self.functions.values().map(f => f.to_string()).join("\n")
  logger.write_string(func_str)
}

///|
pub fn Module::dump(self : Module) -> Unit {
  println(self)
}