defmodule Clip.Ffmpeg do
  @doc false
  require Logger
  import Enum

  @type channel :: :video | :audio
  @type segments :: %{channel() => [String.t()]}

  def ffmpeg_path, do: Application.get_env(:clipper_bot, :ffmpeg, "ffmpeg")

  @doc """
  Assumed sorted, and xor within a channel is the empty set.
  """
  @spec concat(segments, number(), number(), String.t()) ::
          {:ok, size :: pos_integer()} | {:error, any()}
  def concat(segments, ss, t, output) do
    # https://ffmpeg.org/ffmpeg-formats.html#concat

    concat_scripts =
      map(segments, fn
        {:video, segments} -> concat_script_v(segments, ss)
        {:audio, segments} -> concat_script_a(segments, ss)
      end)

    concat_script_header = "ffconcat version 1.0\n"

    concat_files =
      map(concat_scripts, fn script ->
        script = concat_script_header <> script
        concat_path = gen_concat_file_path()
        concat_path_others = Process.get(:concat_files, [])
        Process.put(:concat_files, [concat_path | concat_path_others])
        File.write!(concat_path, script)
        concat_path
      end)

    ffmpeg_args =
      [
        "-hide_banner",
        "-y"
      ] ++
        flat_map(concat_files, fn path ->
          [
            "-f",
            "concat",
            "-safe",
            "0",
            "-i",
            path
          ]
        end) ++
        [
          "-c",
          "copy",
          "-t",
          to_string(t),
          "-movflags",
          "faststart",
          output
        ]

    Logger.debug(Enum.join([ffmpeg_path() | ffmpeg_args], " "))

    {stdout, exit_status} =
      System.cmd(ffmpeg_path(), ffmpeg_args, stderr_to_stdout: true)

    Logger.debug(ffmpeg_stdouterr: stdout)

    case exit_status do
      0 -> {:ok, File.stat!(output).size}
      other -> {:error, other}
    end
  after
    if concat_file = Process.get(:concat_file) do
      each(concat_file, &File.rm/1)
    end
  end

  defp concat_script_v(segments, ss) do
    # Video segments have their timestamps,
    # so the "inpoint" is set as `ss`
    map(segments, fn
      %{path: path, ss: ss_s, to: to_s} ->
        cond do
          ss_s < ss and to_s <= ss ->
            ""

          ss_s < ss and to_s > ss ->
            "file #{sanitized_path(path)}\n" <>
              "inpoint #{ss}\n" <>
              "duration #{to_s - ss}\n"

          ss_s >= ss ->
            "file #{sanitized_path(path)}\n" <>
              "inpoint #{ss_s}\n" <>
              "duration #{to_s - ss_s}\n"
        end
    end)
    |> into("")
  end

  defp concat_script_a(segments, ss) do
    map(segments, fn
      %{path: path, ss: ss_s, to: to_s} ->
        cond do
          ss_s < ss and to_s <= ss ->
            ""

          ss_s < ss and to_s > ss ->
            "file #{sanitized_path(path)}\n" <>
              "inpoint #{ss - ss_s}\n" <>
              "duration #{to_s - ss}\n"

          ss_s >= ss ->
            "file #{sanitized_path(path)}\n" <>
              "inpoint #{0}\n" <>
              "duration #{to_s - ss_s}\n"
        end
    end)
    |> into("")
  end

  defp gen_concat_file_path do
    System.tmp_dir!()
    |> Path.join((:rand.bytes(8) |> Base.encode16()) <> ".ytex.concat")
  end

  defp sanitized_path(path) do
    path = String.replace(path, "'", "\\'")
    "'" <> path <> "'"
  end
end
