lib/cvax.ex

defmodule Cvax do
  # TODO: write docs
  # TODO: write typespecs
  # TODO: write functionality for compound components
  # TODO: refactor implementaations

  @moduledoc """
  Documentation for `Cvax`.
  """

  # cn
  # ============================================
  @spec cn(any) :: binary
  def cn(classes) do
    cx(classes)
  end

  # cx
  # ============================================
  @spec cx(any) :: String.t()
  def cx(classes) when is_binary(classes) do
    classes
    |> String.trim()
    |> String.replace(~r/ +/, " ")
  end

  def cx(class: classes), do: cx(classes)
  def cx({:class, classes}), do: cx(classes)
  def cx(%{:class => classes}), do: cx(classes)

  def cx(classes) when is_list(classes) do
    List.flatten(classes)
    |> Enum.filter(fn x -> !(x == nil || x == true || x == false) end)
    |> Enum.map(fn x -> if(is_binary(x), do: x, else: cx(x)) end)
    |> Enum.join(" ")
    |> String.trim()
    |> String.replace(~r/ +/, " ")
  end

  def cx({true, classes}), do: if(is_binary(classes), do: classes, else: cx(classes))
  def cx(%{true => classes}), do: if(is_binary(classes), do: classes, else: cx(classes))
  def cx(_), do: ""

  # variants
  # ============================================
  @spec compose_variants(%{
          base: String.t(),
          variants: %{},
          default_variants: %{},
          # TODO:
          compound_variants: [%{}]
        }) :: (any() -> String.t())
  def compose_variants(configs) do
    case configs do
      %{
        variants: variants,
        default_variants: default_variants,
        compound_variants: compound_variants
      }
      when is_map(variants) and map_size(variants) > 0 and is_map(default_variants) ->
        fn
          props when is_map(props) and map_size(props) > 0 ->
            get_classes_from_map(props, configs.base, variants, default_variants)
            |> add_compound_varriants(props, compound_variants)

          props when is_list(props) ->
            get_classes_from_list(props, configs.base, variants, default_variants)
            |> add_compound_varriants(props, compound_variants)

          _props ->
            return_defaults(configs)
        end

      %{variants: variants, default_variants: default_variants}
      when is_map(variants) and map_size(variants) > 0 and is_map(default_variants) ->
        fn
          props when is_map(props) and map_size(props) > 0 ->
            get_classes_from_map(props, configs.base, variants, default_variants)

          props when is_list(props) ->
            get_classes_from_list(props, configs.base, variants, default_variants)

          _props ->
            return_defaults(configs)
        end

      %{variants: variants}
      when is_map(variants) and map_size(variants) > 0 ->
        fn
          props when is_map(props) and map_size(props) > 0 ->
            get_classes_from_map(props, configs.base, variants)

          props when is_list(props) ->
            get_classes_from_list(props, configs.base, variants)

          _props ->
            return_defaults(configs)
        end

      %{base: base} ->
        fn
          %{:class => class} -> cx([base, class])
          {:class, class} -> cx([base, class])
          [class: class] -> cx([base, class])
          _ -> cx(base)
        end

      _ ->
        fn
          %{:class => class} -> cx(class)
          {:class, class} -> cx(class)
          [class: class] -> cx(class)
          _ -> ""
        end
    end
  end

  # -- private functions
  # ============================================
  defp add_compound_varriants(classes, _props, _compound_variants) do
    classes
  end

  defp get_classes_from_list(props, base, variants) do
    # get variants
    Enum.reduce(
      props,
      %{defaults: [], variants: [], class: "", configs_variants: variants},
      fn
        {:class, class}, acc ->
          Map.put(acc, :class, class)

        {props_key, props_prop}, acc ->
          case Map.has_key?(variants, props_key) do
            true ->
              Map.put(acc, :variants, [
                acc.variants,
                Map.get(variants[props_key], props_prop)
              ])
              |> Map.put(:configs_variants, Map.delete(acc.configs_variants, props_key))

            false ->
              acc
          end
      end
    )

    # get default variants
    |> then(fn x ->
      Enum.reduce(
        x.configs_variants,
        x,
        fn _, acc ->
          # fn {key, prop}, acc ->
          Map.put(acc, :defaults, [acc.defaults])
        end
      )
    end)

    # stringify variants
    |> then(fn
      acc ->
        cx([base, acc.defaults, acc.variants, acc.class])
    end)
  end

  defp get_classes_from_list(props, base, variants, default_variants) do
    # get variants
    Enum.reduce(
      props,
      %{defaults: [], variants: [], class: "", configs_variants: variants},
      fn
        {:class, class}, acc ->
          Map.put(acc, :class, class)

        {props_key, props_prop}, acc ->
          case Map.has_key?(variants, props_key) do
            #
            true ->
              Map.put(acc, :variants, [
                acc.variants,
                Map.get(variants[props_key], props_prop)
              ])
              |> Map.put(:configs_variants, Map.delete(acc.configs_variants, props_key))

            false ->
              acc
          end
      end
    )

    # get default variants
    |> then(fn x ->
      Enum.reduce(
        x.configs_variants,
        x,
        fn {key, prop}, acc ->
          Map.put(acc, :defaults, [
            acc.defaults,
            Map.get(prop, default_variants[key])
          ])
        end
      )
    end)

    # stringify variants
    |> then(fn
      acc ->
        cx([base, acc.defaults, acc.variants, acc.class])
    end)
  end

  defp get_classes_from_map(props, base, variants) do
    Enum.reduce(variants, %{defaults: [], variants: []}, fn {key, prop}, acc ->
      case Map.get(props, key) do
        nil ->
          %{
            defaults: [
              acc.defaults
            ],
            variants: acc.variants
          }

        prop_key ->
          %{defaults: acc.defaults, variants: [acc.variants, prop[prop_key]]}
      end
    end)
    |> then(fn
      %{defaults: defaults, variants: variants} ->
        cx([base, defaults, variants, Map.get(props, :class)])
    end)
  end

  defp get_classes_from_map(props, base, variants, default_variants) do
    Enum.reduce(variants, %{defaults: [], variants: []}, fn {key, prop}, acc ->
      case Map.get(props, key) do
        nil ->
          %{
            defaults: [acc.defaults, prop[Map.get(default_variants, key)]],
            variants: acc.variants
          }

        prop_key ->
          %{defaults: acc.defaults, variants: [acc.variants, prop[prop_key]]}
      end
    end)
    |> then(fn
      %{defaults: defaults, variants: variants} ->
        cx([base, defaults, variants, Map.get(props, :class)])
    end)
  end

  defp return_defaults(configs) do
    cx([
      configs.base,
      Map.has_key?(configs, :default_variants) &&
        Enum.map(configs.default_variants, fn {key, prop} ->
          Map.get(configs.variants[key], prop)
        end)
    ])
  end
end