lib/naiveical/creator/vcard.ex

defmodule Naiveical.Creator.Vcard do
  @moduledoc """
  Module for creating Vcard(vcf) text files.
  """

  ### This VCARD is generated trought the Thunderbird UI
  ### Which I took as an inspiration for the fields.
  # VCARD Example
  # BEGIN:VCARD
  # VERSION:3.0
  # PRODID:-//Sabre//Sabre VObject 4.3.0//EN
  # N:Name;Surname ;;;
  # FN:Surname  Name
  # NICKNAME:Nickkontakt
  # EMAIL;TYPE=PREF,work;PREF=1:work@klasik.com
  # TEL;TYPE=work:060111111111
  # NOTE:Some notes
  # URL;TYPE=home:https://klasik.com
  # TITLE:Ceo
  # ROLE:Ceo
  # ORG:Company;Department
  # BDAY;VALUE=DATE:19950324
  # UID:688819b9-75ca-42ea-a517-b6ba205274d8
  # URL;TYPE=work:https://klasik.com
  # URL;TYPE=work:https://klasik.com
  # URL;TYPE=work:https://klasik.com
  # ADR;TYPE=work:;;Address 1;City;State/Province;11000;Serbia
  # ADR:;;Address 2;City 2;State 2;159999;Serbia
  # END:VCARD

  @date_format "{YYYY}{0M}{0D}"
  @vcard_version "4.0"
  @prod_id "-//Migadu-Excalt//"
  @doc """
  Create a simple VCard file.
  VERSION and FN property must be included in vcard object.
  NOTE: in our case, which we are using SabreDav as a Cardav server,
  version and product ID will be overwritten by server itself.
  """
  @spec create_vcard(opts :: Keyword.t()) :: String.t()
  def create_vcard(uuid, opts \\ []) do
    first_name = Keyword.get(opts, :first_name, "")
    last_name = Keyword.get(opts, :last_name, "")
    middle_name = Keyword.get(opts, :middle_name, "")
    prefix = Keyword.get(opts, :prefix, "")
    suffix = Keyword.get(opts, :suffix, "")
    email = Keyword.get(opts, :email, "") |> create_email([])
    note = Keyword.get(opts, :note, "") |> create_note([])

    display_name =
      Keyword.get(opts, :display_name, "") |> create_display_name(first_name, last_name)

    tel = Keyword.get(opts, :tel, "") |> create_telephone([])
    addresses = Keyword.get(opts, :address, "") |> create_address([])
    nickname = Keyword.get(opts, :nickname, "") |> create_nickname([])
    title = Keyword.get(opts, :title, "") |> create_title([])
    role = Keyword.get(opts, :role, "") |> create_role([])
    organization = Keyword.get(opts, :organization, "") |> create_organization([])
    website = Keyword.get(opts, :website, "") |> create_website([])
    birthday = Keyword.get(opts, :birthday, "") |> create_special_date(:bday, [])
    anniversary = Keyword.get(opts, :anniversary, "") |> create_special_date(:anniversary, [])
    kind = Keyword.get(opts, :kind, "") |> create_kind([])
    categories = Keyword.get(opts, :categories, "") |> create_categories([])
    name = create_full_name(prefix, first_name, middle_name, last_name, suffix)

    ("""
     BEGIN:VCARD
     VERSION:#{@vcard_version}
     PRODID:#{@prod_id}
     UID:#{uuid}
     FN:#{display_name}
     """ <>
       name <>
       email <>
       tel <>
       addresses <>
       nickname <>
       organization <>
       title <>
       role <>
       website <>
       anniversary <>
       birthday <>
       note <>
       categories <>
       kind <>
       """
       END:VCARD
       """)
    |> String.replace(~r/\r?\n/, "\r\n")
  end

  @doc """
  Creates catagories, also know as tags. Categories are comma separated like CSV.
  Can be used to group vcards.
  Reference [RFC 6350 Section 6.7.1](https://www.rfc-editor.org/rfc/rfc6350#section-6.7.1)
  """
  @spec create_categories(categories :: String.t(), opts :: List.t()) :: String.t()
  def create_categories(categories, opts \\ [])
  def create_categories("", _), do: ""

  def create_categories(categories, []),
    do: "CATEGORIES:" <> trim_categories(categories) <> "\r\n"

  def create_categories(categories, opts) do
    categories = trim_categories(categories)

    params =
      Enum.reduce(opts, "CATEGORIES", fn {key, value}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{value}"
      end)

    params <> ":#{categories}\r\n"
  end

  @doc """
  Simple note to add more information about the VCARD.
  Reference [RFC 6350 Section 6.7.2](https://www.rfc-editor.org/rfc/rfc6350#section-6.7.2)
  """
  def create_note(note, opts \\ [])
  def create_note("", _), do: ""

  def create_note(note, []) do
      note = Naiveical.Helpers.fold(note)
      "NOTE:#{note}\r\n"
  end

  def create_note(note, opts) do
    note = Naiveical.Helpers.fold(note)
    params =
      Enum.reduce(opts, "NOTE", fn {key, value}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{value}"
      end)

    params <> ":#{note}\r\n"
  end

  @doc """
  Creates NICKNAME, can be random text.
  """
  @spec create_nickname(nickname :: String.t(), opts :: List.t()) :: String.t()
  def create_nickname(nickname, opts \\ [])
  def create_nickname("", _), do: ""
  def create_nickname(nickname, _), do: "NICKNAME:#{nickname}\r\n"

  @doc """
  Creates TITLE, it is most often associated with organization.
  """
  @spec create_title(nickname :: String.t(), opts :: List.t()) :: String.t()
  def create_title(title, opts \\ [])
  def create_title("", _), do: ""
  def create_title(title, _), do: "TITLE:#{title}\r\n"

  @doc """
  Creates ROLE, it is most often associated with organization.
  """
  @spec create_role(nickname :: String.t(), opts :: List.t()) :: String.t()
  def create_role(role, opts \\ [])
  def create_role("", _), do: ""
  def create_role(role, _), do: "ROLE:#{role}\r\n"

  @doc """
  Creates organization.
  """
  @spec create_organization(nickname :: String.t(), opts :: List.t()) :: String.t()
  def create_organization(org, opts \\ [])
  def create_organization("", _), do: ""
  def create_organization(org, _), do: "ORG:#{org}\r\n"

  @doc """
  Creates special dates such as birthdays or anniversary.
  """
  @spec create_special_date(nickname :: String.t(), opts :: List.t()) :: String.t()
  def create_special_date(date, type, opts \\ [])
  def create_special_date("", _, _), do: ""

  def create_special_date(%Date{} = date, type, []) when not is_nil(date) do
    date = Timex.format!(date, @date_format)
    type = upcase_key(type)
    "#{type}:#{date}"
  end

  @doc """
  Creates a KIND for vcard.
  Please refer to [RFC Section 6.1.4](https://www.rfc-editor.org/rfc/rfc6350#section-6.1.4)
  """
  @spec create_kind(nickname :: String.t(), opts :: List.t()) :: String.t()
  def create_kind(kind, opts \\ [])
  def create_kind("", _), do: ""

  def create_kind(kind, opts) do
    params =
      Enum.reduce(opts, "KIND", fn {key, value}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{value}"
      end)

    params <> ":#{kind}\r\n"
  end

  @doc """
  Creates an email to put in VCARD file.
  Pass a keyword list of params(options) to add params to email address you are creating.
  Please refer the [RFC 6350 Section 6.4.2](https://www.rfc-editor.org/rfc/rfc6350#section-6.4.2) for more details about the supported params.

  ## Example
      iex> Naiveical.Creator.Vcard.create_email("email@domain.tld", type: "work", pref: 1, label: "good")
      EMAIL;TYPE=work;PREF=1;LABEL=good:email@domain.tld
  """
  @spec create_email(address :: String.t(), opts :: List.t()) :: String.t()
  def create_email(address, opts \\ [])
  def create_email("", []), do: ""
  def create_email(address, []), do: "EMAIL:#{address}\r\n"

  def create_email(address, opts) do
    params =
      Enum.reduce(opts, "EMAIL", fn {key, val}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{val}"
      end)

    params <> ":#{address}\r\n"
  end

  @doc """
  Creates display name, which is FN property defined by [RFC 6350 Section 6.2.1](https://www.rfc-editor.org/rfc/rfc6350#section-6.2.1)

  ## Example
      iex> Naiveical.Creator.Vcard.create_display_name("Display Name")
      "Display Name"
  """
  def create_display_name(display_name \\ "", first_name \\ "", last_name \\ "") do
    if display_name != "" do
      display_name
    else
      "#{first_name} #{last_name}"
    end
    |> String.trim()
  end

  @doc """
  Creates address property. Address property has 7 list-components, where first two are ommited.
  Please refer to [RFC 6350 Section 6.3.1](https://www.rfc-editor.org/rfc/rfc6350#section-6.3.1)
  """
  @spec create_address(address :: String.t(), opts :: List.t()) :: String.t()
  def create_address(address, opts \\ [])
  def create_address("", []), do: ""
  def create_address(address, []), do: "ADR:;;#{address};;;;\r\n"

  def create_address(address, opts) do
    Enum.reduce(opts, "ADR", fn {key, val}, acc ->
      # this is check whether the key is not an address component
      if key not in [:street, :city, :region, :postal_code, :country] do
        key = upcase_key(key)
        acc <> ";#{key}=#{val}"
      else
        acc
      end
    end)
    |> add_addresses(address, opts)
  end

  @doc """
  Creates URL property.
  Reference [RFC 6350 Section 6.7.8](https://www.rfc-editor.org/rfc/rfc6350#section-6.7.8)
  ## Example
      iex> Naiveical.Creator.Vcard.create_website("https://example.org", type: "work")
      URL;TYPE=work:https://example.org\r\n
  """
  @spec create_website(url :: String.t(), opts :: List.t()) :: String.t()
  def create_website(url, opts \\ [])
  def create_website("", []), do: ""
  def create_website(url, []), do: "URL:#{url}\r\n"

  def create_website(url, opts) do
    params =
      Enum.reduce(opts, "URL", fn {key, value}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{value}"
      end)

    params <> ":#{url}\r\n"
  end

  @doc """
  Creates a full name for VCARD.
  N property consist of 5 list-components.
  N:PREFIX;FIRSTNAME;MIDDLENAME;LASTNAME;SUFFIX
  Reference[RFC Idenitification Properties N SECTION 6.2.2](https://www.rfc-editor.org/rfc/rfc6350#section-6.2.2)
  ## Example
      iex> Naiveical.Creator.Vcard.create_full_name("", "User", "Middle", "Surname", "", value: "text")
      N;VALUE=text:;User;Middle;Surname;;
  """
  @spec create_full_name(
          prefix :: String.t(),
          first_name :: String.t(),
          middle_name :: String.t(),
          last_name :: String.t(),
          suffix :: String.t(),
          opts :: List.t()
        ) :: String.t()
  def create_full_name(prefix, first_name, middle_name, last_name, suffix, opts \\ [])

  def create_full_name(prefix, first_name, middle_name, last_name, suffix, []) do
    if should_construct_full_name?([prefix, first_name, middle_name, last_name, suffix]) do
      "N" <> construct_full_name(prefix, first_name, middle_name, last_name, suffix)
    else
      ""
    end
  end

  def create_full_name(prefix, first_name, middle_name, last_name, suffix, opts) do
    params =
      Enum.reduce(opts, "N", fn {key, value}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{value}"
      end)

    if should_construct_full_name?([prefix, first_name, middle_name, last_name, suffix]) do
      params <> construct_full_name(prefix, first_name, middle_name, last_name, suffix)
    else
      ""
    end
  end

  @doc """
  Creates telephone property for VCARD.
  Reference [RFC Section 6.4.1](https://www.rfc-editor.org/rfc/rfc6350#section-6.4.1)
  ## Example
      iex> Naiveical.Creator.Vcard.create_telephone("1234567890", type: "work", pref: 1)
      TEL;TYPE=work;PREF=1:1234567890
  """
  @spec create_telephone(tel :: String.t(), opts :: List.t()) :: String.t()
  def create_telephone(tel, opts \\ [])
  def create_telephone("", _), do: ""
  def create_telephone(tel, []), do: "TEL:#{tel}\r\n"

  def create_telephone(tel, opts) do
    params =
      Enum.reduce(opts, "TEL", fn {key, value}, acc ->
        key = upcase_key(key)
        acc <> ";#{key}=#{value}"
      end)

    params <> ":#{tel}\r\n"
  end

  ### Helpers for creation functions. ###

  defp add_addresses(address_comp, address, opts) do
    # We need to take care of the places for the address component
    # street;city;region;code;country
    city = Keyword.get(opts, :city, "")
    region = Keyword.get(opts, :region, "")
    postal_code = Keyword.get(opts, :postal_code, "")
    country = Keyword.get(opts, :country, "")

    "#{address_comp}:;;#{address};#{city};#{region};#{postal_code};#{country}\r\n"
  end

  defp should_construct_full_name?(name_comps) do
    Enum.any?(name_comps, fn x -> x != "" end)
  end

  defp construct_full_name(prefix, first_name, middle_name, last_name, suffix) do
    if should_construct_full_name?([prefix, first_name, middle_name, last_name, suffix]) do
      ":#{prefix};#{first_name};#{middle_name};#{last_name};#{suffix}\r\n"
    else
      ""
    end
  end

  defp trim_categories(categories) do
    categories
    |> String.split(",")
    |> Enum.reject(&(&1 == "" or is_nil(&1)))
    |> Enum.map_join(",", &String.trim/1)
  end

  defp upcase_key(key) do
    key
    |> to_string()
    |> String.upcase()
  end
end