defmodule Zig.C do
  @moduledoc false

  # module creates a struct that defines the options for
  # c-interoperability with zigler

  defstruct include_dirs: [],
            library_dirs: [],
            src: [],
            link_lib: [],
            link_libcpp: false

  alias Zig.Options

  @type t :: %__MODULE__{
          include_dirs: [String.t() | {:system, String.t()}],
          library_dirs: [String.t() | {:system, String.t()}],
          link_lib: [String.t() | {:system, String.t()}],
          link_libcpp: boolean,
          src: [{String.t(), [String.t()]}]
        }

  @spec new(Zig.c_options(), Options.context()) :: t
  def new(opts, context) do
    opts
    |> Options.scrub_non_keyword(context)
    |> Options.normalize_kw(:include_dirs, [], &normalize_pathlist/2, context)
    |> Options.normalize_kw(:library_dirs, [], &normalize_pathlist/2, context)
    |> Options.normalize_kw(:link_lib, [], &normalize_pathlist/2, context)
    |> Options.normalize_kw(:src, [], &normalize_c_src/2, context)
    |> Options.validate(:link_libcpp, :boolean, context)
    |> then(&struct!(__MODULE__, &1))
  rescue
    e in KeyError ->
      Options.raise_with("was supplied the invalid option `#{e.key}`", context)
  end

  def normalize_pathlist([n | _] = charlist, context) when is_integer(n),
    do: [resolve_path("#{charlist}", context)]

  def normalize_pathlist(path_or_paths, context) do
    path_or_paths
    |> List.wrap()
    |> Enum.map(&resolve_path(&1, context))
  end

  defp resolve_path({:system, path}, context) do
    {:system, IO.iodata_to_binary(path)}
  rescue
    _ in ArgumentError ->
      Options.raise_with("system path must be iodata", path, context)
  end

  defp resolve_path({:priv, path}, context) do
    path
    |> IO.iodata_to_binary()
    |> Path.expand(:code.priv_dir(context.otp_app))
  rescue
    _ in ArgumentError ->
      Options.raise_with("priv path must be iodata", path, context)
  end

  defp resolve_path(path, context) do
    case IO.iodata_to_binary(path) do
      "./" <> rest ->
        {:system, Path.join(File.cwd!(), rest)}

      path ->
        Path.expand(path, Path.dirname(context.file))
    end
  rescue
    _ in ArgumentError ->
      Options.raise_with(
        "must be path, `{:priv, path}`, `{:system, path}`, or a list of those",
        path,
        context
      )
  end

  def normalize_c_src([n | _] = charlist, context) when is_integer(n),
    do: [{resolve_path("#{charlist}", context), []}]

  def normalize_c_src(src_or_srcs, context) do
    src_or_srcs
    |> List.wrap()
    |> Enum.flat_map(fn src_def ->
      src_def
      |> resolve_src(context)
      |> expand_wildcards()
    end)
  end

  defp resolve_src({tag, _} = path, context) when tag in ~w[priv system]a do
    path
    |> resolve_path(context)
    |> normalize_src_options([], context)
  end

  defp resolve_src({tag, path, options}, context) when tag in ~w[priv system]a do
    {tag, path}
    |> resolve_path(context)
    |> normalize_src_options(options, context)
  end

  defp resolve_src({path, options}, context) do
    path
    |> IO.iodata_to_binary()
    |> Path.expand(Path.dirname(context.file))
    |> normalize_src_options(options, context)
  rescue
    _ in ArgumentError ->
      Options.raise_with("source files must be iodata", path, context)
  end

  defp resolve_src(path, context) do
    path
    |> IO.iodata_to_binary()
    |> Path.expand(Path.dirname(context.file))
    |> normalize_src_options([], context)
  rescue
    _ in ArgumentError ->
      Options.raise_with(
        "must be path, `{path, [opts]}`, `{:priv, path}`, `{:priv, path, [opts]}`, `{:system, path}`, `{:system, path, [opts]}`, or a list of those",
        path,
        context
      )
  end

  defp normalize_src_options(path, opts, context) when is_list(opts) do
    {path, Enum.map(opts, &IO.iodata_to_binary/1)}
  rescue
    _ in ArgumentError ->
      Options.raise_with(
        "must be a list of iodata",
        opts,
        Options.push_key(context, inspect(path))
      )
  end

  defp expand_wildcards({{:system, path}, opts}) do
    {path, opts}
    |> expand_wildcards()
    |> Enum.map(fn {path, opts} -> {{:system, path}, opts} end)
  end

  defp expand_wildcards({path, opts} = spec) do
    if String.ends_with?(path, "/*") do
      dir = Path.dirname(path)

      dir
      |> File.ls!()
      |> Enum.map(&{Path.join(dir, &1), opts})
    else
      [spec]
    end
  end
end
