defmodule Tzdata.DataBuilder do
  @moduledoc false
  alias Tzdata.DataLoader
  alias Tzdata.PeriodBuilder
  alias Tzdata.LeapSecParser
  require Logger

  # download new data releases, then parse them, build
  # periods and save the data in an ETS table
  def load_and_save_table do
    {:ok, content_length, release_version, tzdata_dir, modified_at} = DataLoader.download_new()
    current_version = Tzdata.ReleaseReader.release_version()

    if release_version == current_version do
      # remove temporary tzdata dir
      File.rm_rf(tzdata_dir)

      Logger.info(
        "Downloaded tzdata release from IANA is the same version as the version currently in use (#{
          current_version
        })."
      )

      {:error, :downloaded_version_same_as_current_version}
    else
      do_load_and_save_table(content_length, release_version, tzdata_dir, modified_at)
    end
  end

  defp do_load_and_save_table(content_length, release_version, tzdata_dir, modified_at) do
    ets_table_name = ets_table_name_for_release_version(release_version)
    table = :ets.new(ets_table_name, [:bag, :named_table])
    {:ok, map} = Tzdata.BasicDataMap.from_files_in_dir(tzdata_dir)
    :ets.insert(table, {:release_version, release_version})
    :ets.insert(table, {:archive_content_length, content_length})
    :ets.insert(table, {:rules, map.rules})
    :ets.insert(table, {:zones, map.zones})
    :ets.insert(table, {:links, map.links})
    :ets.insert(table, {:zone_list, map.zone_list})
    :ets.insert(table, {:link_list, map.link_list})
    :ets.insert(table, {:zone_and_link_list, map.zone_and_link_list})
    :ets.insert(table, {:by_group, map.by_group})
    :ets.insert(table, {:leap_sec_data, leap_sec_data(tzdata_dir)})
    :ets.insert(table, {:modified_at, modified_at})

    map.zone_list
    |> Enum.each(fn zone_name ->
         insert_periods_for_zone(table, map, zone_name)
       end)

    # remove temporary tzdata dir
    File.rm_rf(tzdata_dir)
    ets_tmp_file_name = "#{release_dir()}/#{release_version}.tmp"
    ets_file_name = ets_file_name_for_release_version(release_version)
    File.mkdir_p(release_dir())
    # Create file using a .tmp line ending to avoid it being
    # recognized as a complete file before writing to it is complete.
    :ets.tab2file(table, :erlang.binary_to_list(ets_tmp_file_name))
    :ets.delete(table)
    # Then rename it, which should be an atomic operation.
    :file.rename(ets_tmp_file_name, ets_file_name)
    {:ok, content_length, release_version}
  end

  defp leap_sec_data(tzdata_dir), do: LeapSecParser.read_file(tzdata_dir)

  def ets_file_name_for_release_version(release_version) do
    "#{release_dir()}/#{release_version}.v#{Tzdata.EtsHolder.file_version()}.ets"
  end

  def ets_table_name_for_release_version(release_version) do
    String.to_atom("tzdata_rel_#{release_version}")
  end

  defp insert_periods_for_zone(table, map, zone_name) do
    key = String.to_atom(zone_name)
    periods = PeriodBuilder.calc_periods(map, zone_name)

    tuple_periods =
      periods
      |> Enum.map(fn period ->
           period_to_tuple(key, period)
         end)

    tuple_periods |> Enum.each(fn tuple_period ->
      :ets.insert(table, tuple_period)
    end)
  end

  defp period_to_tuple(key, period) do
    {
      key,
      period.from.utc,
      period.from.wall,
      period.from.standard,
      period.until.utc,
      period.until.wall,
      period.until.standard,
      period.utc_off,
      period.std_off,
      period.zone_abbr
    }
  end

  defp release_dir do
    Tzdata.Util.data_dir() <> "/release_ets"
  end
end
