///|
/// Shared validation errors for core domain constructors.
#derive(Eq, Debug)
pub(all) enum ValidationError {
  EmptyAddress
  MissingAtSign
  MultipleAtSigns
  MissingLocalPart
  MissingDomain
  UnexpectedWhitespace
  EmptyHeaderName
  IllegalHeaderNameChar(Char)
  IllegalHeaderValueChar(Char)
  EmptyRecipients
  EmptyMessageRecipients
  EmptyMultipart
  AlternativeNeedsTwoParts
  AlternativeRequiresTextParts
  EmptyFilename
  EmptyContentId
}

///|
#derive(Eq, Debug)
pub enum Charset {
  Utf8
  UsAscii
  Other(String)
}

///|
#derive(Eq, Debug)
pub enum TransferEncoding {
  SevenBit
  EightBit
  QuotedPrintable
  Base64
  Binary
}

///|
#derive(Eq, Debug)
pub enum DispositionKind {
  Inline
  Attachment
  Other(String)
}

///|
#derive(Eq, Debug)
pub enum MultipartKind {
  Mixed
  Alternative
  Related
  Custom(String)
}

///|
#derive(Eq, Debug)
pub struct HeaderParam {
  name : String
  value : String
}

///|
#derive(Eq, Debug)
pub struct ContentType {
  top_level : String
  sub_type : String
  parameters : Array[HeaderParam]
}

///|
#derive(Eq, Debug)
pub struct ContentId {
  value : String
}

///|
#derive(Eq, Debug)
pub struct ContentDisposition {
  kind : DispositionKind
  filename : String?
  parameters : Array[HeaderParam]
}

///|
#derive(Eq, Debug)
pub struct Mailbox {
  display_name : String?
  address : String
}

///|
#derive(Eq, Debug)
pub struct Header {
  name : String
  value : String
}

///|
#derive(Eq, Debug)
pub struct TextBody {
  content_type : ContentType
  charset : Charset
  transfer_encoding : TransferEncoding
  content : String
}

///|
#derive(Eq, Debug)
pub enum AttachmentSource {
  InMemory(Array[Byte])
  FilePath(String)
}

///|
#derive(Eq, Debug)
pub struct BinaryBody {
  content_type : ContentType
  disposition : ContentDisposition
  transfer_encoding : TransferEncoding
  filename : String?
  content_id : ContentId?
  source : AttachmentSource
}

///|
#derive(Eq, Debug)
pub enum Body {
  Text(TextBody)
  Binary(BinaryBody)
  Multipart(MultipartKind, Array[Body])
}

///|
#derive(Eq, Debug)
pub struct Message {
  from : Mailbox
  to : Array[Mailbox]
  cc : Array[Mailbox]
  bcc : Array[Mailbox]
  subject : String
  headers : Array[Header]
  body : Body
}

///|
#derive(Eq, Debug)
pub struct Envelope {
  mail_from : Mailbox
  rcpt_to : Array[Mailbox]
}

///|
pub fn ContentType::new(
  top_level : String,
  sub_type : String,
  parameters : Array[HeaderParam],
) -> ContentType {
  { top_level, sub_type, parameters, }
}

///|
pub fn ContentType::text_plain() -> ContentType {
  ContentType::new("text", "plain", [])
}

///|
pub fn ContentType::text_html() -> ContentType {
  ContentType::new("text", "html", [])
}

///|
pub fn ContentType::application_octet_stream() -> ContentType {
  ContentType::new("application", "octet-stream", [])
}

///|
pub fn Charset::utf8() -> Charset {
  Utf8
}

///|
pub fn Charset::us_ascii() -> Charset {
  UsAscii
}

///|
pub fn Charset::other(name : String) -> Charset {
  Other(name)
}

///|
pub fn TransferEncoding::seven_bit() -> TransferEncoding {
  SevenBit
}

///|
pub fn TransferEncoding::eight_bit() -> TransferEncoding {
  EightBit
}

///|
pub fn TransferEncoding::quoted_printable() -> TransferEncoding {
  QuotedPrintable
}

///|
pub fn TransferEncoding::base64() -> TransferEncoding {
  Base64
}

///|
pub fn TransferEncoding::binary() -> TransferEncoding {
  Binary
}

///|
pub fn DispositionKind::inline() -> DispositionKind {
  Inline
}

///|
pub fn DispositionKind::attachment() -> DispositionKind {
  Attachment
}

///|
pub fn DispositionKind::other(name : String) -> DispositionKind {
  Other(name)
}

///|
pub fn MultipartKind::mixed() -> MultipartKind {
  Mixed
}

///|
pub fn MultipartKind::alternative() -> MultipartKind {
  Alternative
}

///|
pub fn MultipartKind::related() -> MultipartKind {
  Related
}

///|
pub fn MultipartKind::custom(name : String) -> MultipartKind {
  Custom(name)
}

///|
pub fn AttachmentSource::in_memory(data : Array[Byte]) -> AttachmentSource {
  InMemory(data)
}

///|
pub fn AttachmentSource::file_path(path : String) -> AttachmentSource {
  FilePath(path)
}

///|
pub fn HeaderParam::new(name : String, value : String) -> HeaderParam {
  { name, value, }
}

///|
pub fn ContentId::new(value : String) -> Result[ContentId, ValidationError] {
  if value == "" {
    Err(EmptyContentId)
  } else {
    Ok({ value, })
  }
}

///|
pub fn ContentDisposition::new(
  kind : DispositionKind,
  filename? : String,
  parameters~ : Array[HeaderParam] = [],
) -> Result[ContentDisposition, ValidationError] {
  match filename {
    Some(name) if name == "" => Err(EmptyFilename)
    _ => Ok({ kind, filename, parameters, })
  }
}

///|
pub fn Header::new(
  name : String,
  value : String,
) -> Result[Header, ValidationError] {
  match validate_header_name(name) {
    Ok(_) =>
      match validate_header_value(value) {
        Ok(_) => Ok({ name, value, })
        Err(error) => Err(error)
      }
    Err(error) => Err(error)
  }
}

///|
pub fn Mailbox::new(
  address : String,
  display_name : String?,
) -> Result[Mailbox, ValidationError] {
  match validate_address(address) {
    Ok(_) => Ok({ display_name, address, })
    Err(error) => Err(error)
  }
}

///|
pub fn Mailbox::address_only(
  address : String,
) -> Result[Mailbox, ValidationError] {
  Mailbox::new(address, None)
}

///|
pub fn Body::plain_text(content : String) -> Body {
  Text({
    content_type: ContentType::text_plain(),
    charset: Utf8,
    transfer_encoding: QuotedPrintable,
    content,
  })
}

///|
pub fn Body::html(content : String) -> Body {
  Text({
    content_type: ContentType::text_html(),
    charset: Utf8,
    transfer_encoding: QuotedPrintable,
    content,
  })
}

///|
pub fn Body::text(
  content_type : ContentType,
  charset : Charset,
  transfer_encoding : TransferEncoding,
  content : String,
) -> Body {
  Text({ content_type, charset, transfer_encoding, content, })
}

///|
pub fn Body::attachment(
  filename : String,
  data : Array[Byte],
) -> Result[Body, ValidationError] {
  make_binary_body(
    filename,
    ContentType::application_octet_stream(),
    Attachment,
    None,
    InMemory(data),
  )
}

///|
pub fn Body::inline_asset(
  content_id : String,
  filename : String,
  content_type : ContentType,
  data : Array[Byte],
) -> Result[Body, ValidationError] {
  make_binary_body(
    filename,
    content_type,
    Inline,
    Some(content_id),
    InMemory(data),
  )
}

///|
pub fn Body::attachment_file(
  filename : String,
  path : String,
  content_type~ : ContentType = ContentType::application_octet_stream(),
) -> Result[Body, ValidationError] {
  make_binary_body(filename, content_type, Attachment, None, FilePath(path))
}

///|
pub fn Body::inline_asset_file(
  content_id : String,
  filename : String,
  path : String,
  content_type : ContentType,
) -> Result[Body, ValidationError] {
  make_binary_body(
    filename,
    content_type,
    Inline,
    Some(content_id),
    FilePath(path),
  )
}

///|
pub fn Body::binary(
  content_type : ContentType,
  disposition : ContentDisposition,
  transfer_encoding : TransferEncoding,
  filename : String?,
  content_id : ContentId?,
  source : AttachmentSource,
) -> Result[Body, ValidationError] {
  if filename is Some(name) && name == "" {
    return Err(EmptyFilename)
  }
  if content_id is Some(id) && id.value == "" {
    return Err(EmptyContentId)
  }
  Ok(
    Binary({
      content_type,
      disposition,
      transfer_encoding,
      filename,
      content_id,
      source,
    }),
  )
}

///|
pub fn Body::mixed(parts : Array[Body]) -> Result[Body, ValidationError] {
  make_multipart(Mixed, parts)
}

///|
pub fn Body::alternative(parts : Array[Body]) -> Result[Body, ValidationError] {
  make_multipart(Alternative, parts)
}

///|
pub fn Body::related(parts : Array[Body]) -> Result[Body, ValidationError] {
  make_multipart(Related, parts)
}

///|
pub fn Body::multipart(
  kind : MultipartKind,
  parts : Array[Body],
) -> Result[Body, ValidationError] {
  make_multipart(kind, parts)
}

///|
pub fn Message::new(
  from : Mailbox,
  to : Array[Mailbox],
  cc : Array[Mailbox],
  bcc : Array[Mailbox],
  subject : String,
  body : Body,
  headers : Array[Header],
) -> Result[Message, ValidationError] {
  if to.is_empty() && cc.is_empty() && bcc.is_empty() {
    return Err(EmptyMessageRecipients)
  }
  Ok({ from, to, cc, bcc, subject, headers, body, })
}

///|
pub fn Message::all_recipients(self : Message) -> Array[Mailbox] {
  let all : Array[Mailbox] = []
  for mailbox in self.to {
    all.push(mailbox)
  }
  for mailbox in self.cc {
    all.push(mailbox)
  }
  for mailbox in self.bcc {
    all.push(mailbox)
  }
  all
}

///|
pub fn Envelope::new(
  mail_from : Mailbox,
  rcpt_to : Array[Mailbox],
) -> Result[Envelope, ValidationError] {
  let normalized = normalize_mailboxes(rcpt_to)
  if normalized.is_empty() {
    return Err(EmptyRecipients)
  }
  Ok({ mail_from, rcpt_to: normalized, })
}

///|
pub fn Envelope::from_message(
  mail_from : Mailbox,
  message : Message,
) -> Result[Envelope, ValidationError] {
  Envelope::new(mail_from, message.all_recipients())
}

///|
fn make_multipart(
  kind : MultipartKind,
  parts : Array[Body],
) -> Result[Body, ValidationError] {
  match validate_multipart(kind, parts) {
    Ok(_) => Ok(Multipart(kind, parts))
    Err(error) => Err(error)
  }
}

///|
fn make_binary_body(
  filename : String,
  content_type : ContentType,
  disposition_kind : DispositionKind,
  content_id : String?,
  source : AttachmentSource,
) -> Result[Body, ValidationError] {
  if filename == "" {
    return Err(EmptyFilename)
  }
  let normalized_content_id = match content_id {
    Some(value) =>
      if value == "" {
        return Err(EmptyContentId)
      } else {
        Some({ value, })
      }
    None => None
  }
  Ok(
    Binary({
      content_type,
      disposition: {
        kind: disposition_kind,
        filename: Some(filename),
        parameters: [],
      },
      transfer_encoding: Base64,
      filename: Some(filename),
      content_id: normalized_content_id,
      source,
    }),
  )
}

///|
fn validate_multipart(
  kind : MultipartKind,
  parts : Array[Body],
) -> Result[Unit, ValidationError] {
  if parts.is_empty() {
    return Err(EmptyMultipart)
  }
  match kind {
    Alternative => {
      if parts.length() < 2 {
        return Err(AlternativeNeedsTwoParts)
      }
      for part in parts {
        match part {
          Text(_) => ()
          _ => return Err(AlternativeRequiresTextParts)
        }
      }
      Ok(())
    }
    _ => Ok(())
  }
}

///|
fn normalize_mailboxes(mailboxes : Array[Mailbox]) -> Array[Mailbox] {
  let normalized : Array[Mailbox] = []
  for mailbox in mailboxes {
    if contains_mailbox_address(normalized, mailbox.address) {
      ()
    } else {
      normalized.push(mailbox)
    }
  }
  normalized
}

///|
fn contains_mailbox_address(
  mailboxes : Array[Mailbox],
  address : String,
) -> Bool {
  for mailbox in mailboxes {
    if mailbox.address == address {
      return true
    }
  }
  false
}

///|
fn validate_header_name(name : String) -> Result[Unit, ValidationError] {
  if name == "" {
    return Err(EmptyHeaderName)
  }
  for c in name.iter() {
    if c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      return Err(IllegalHeaderNameChar(c))
    }
  }
  Ok(())
}

///|
fn validate_header_value(value : String) -> Result[Unit, ValidationError] {
  for c in value.iter() {
    if c == '\n' || c == '\r' {
      return Err(IllegalHeaderValueChar(c))
    }
  }
  Ok(())
}

///|
fn validate_address(address : String) -> Result[Unit, ValidationError] {
  if address == "" {
    return Err(EmptyAddress)
  }
  if contains_address_whitespace(address) {
    return Err(UnexpectedWhitespace)
  }
  let mut at_count = 0
  let mut at_index = -1
  for index, c in address.iter2() {
    if c == '@' {
      at_count = at_count + 1
      at_index = index
    }
  }
  if at_count == 0 {
    return Err(MissingAtSign)
  }
  if at_count > 1 {
    return Err(MultipleAtSigns)
  }
  let local_part = address[:at_index].to_owned()
  let domain = address[at_index + 1:].to_owned()
  if local_part == "" {
    return Err(MissingLocalPart)
  }
  if domain == "" {
    return Err(MissingDomain)
  }
  Ok(())
}

///|
fn contains_address_whitespace(address : String) -> Bool {
  for c in address.iter() {
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      return true
    }
  }
  false
}