if Code.ensure_loaded?(SweetXml) do
  defmodule Mix.Tasks.Zwave.Gen.XmlMappings do
    @moduledoc """
    Generates a module that provides mappings based on the data in `ZWave_custom_cmd_classes.xml`.

      mix zwave.gen.xml_mappings

    ### Flags

    * `--input <path>` - reads the xml from the provided path
    * `--dry-run` - print the generated module out to the console
    * `--output <path>` - writes the generated module at the provided path
    """

    use Mix.Task

    import SweetXml

    @shortdoc "Generates a module that provides mappings based on the data in `ZWave_custom_cmd_classes.xml`"

    @switches [dry_run: :boolean]

    @skip_command_classes [:zwave_cmd_class, :zwave_long_range]
    @command_class_name_overrides %{
      0x4F => :zip_6lowpan,
      0x59 => :association_group_info,
      0x96 => :av_content_renderer_status
    }

    def run(args) do
      {opts, _, _} = OptionParser.parse(args, switches: @switches)

      input_path = Keyword.get(opts, :input, "resources/ZWave_custom_cmd_classes.xml")

      doc = input_path |> File.read!() |> parse()

      command_class_mappings = build_macro(:command_class_mappings, command_class_mappings(doc))

      basic_device_class_mappings =
        build_macro(:basic_device_class_mappings, basic_device_class_mappings(doc))

      generic_device_class_mappings =
        build_macro(:generic_device_class_mappings, generic_device_class_mappings(doc))

      specific_device_class_mappings =
        build_macro(:specific_device_class_mappings, specific_device_class_mappings(doc))

      module =
        quote do
          defmodule Grizzly.ZWave.GeneratedMappings do
            @moduledoc false

            unquote(command_class_mappings)
            unquote(basic_device_class_mappings)
            unquote(generic_device_class_mappings)
            unquote(specific_device_class_mappings)
          end
        end

      module_code = module |> Macro.to_string() |> Code.format_string!()

      module_code =
        """
        # !! THIS FILE IS GENERATED BY `mix zwave.gen.xml`
        # !! DO NOT EDIT DIRECTLY

        #{module_code}
        """

      if opts[:dry_run] do
        Mix.shell().info(module_code)
      else
        output_path = Keyword.get(opts, :output, "lib/grizzly/zwave/generated_mappings.ex")

        Mix.Generator.create_file(output_path, module_code)
      end
    end

    defp build_macro(name, value) do
      quote do
        def unquote(name)(), do: unquote(value)
      end
    end

    defp command_class_mappings(doc) do
      doc
      |> xpath(~x"//zw_classes/cmd_class"l, key: ~x"@key"s, name: ~x"@name"s)
      |> Enum.map(key_name_mapper("COMMAND_CLASS_", @command_class_name_overrides))
      |> Enum.uniq_by(fn {key, _} -> key end)
      |> Enum.reject(&(elem(&1, 1) in @skip_command_classes))
      |> Enum.sort()
    end

    defp basic_device_class_mappings(doc) do
      doc
      |> xpath(~x"//zw_classes/bas_dev"l, key: ~x"@key"s, name: ~x"@name"s)
      |> Enum.map(key_name_mapper("BASIC_TYPE_"))
      |> Enum.sort()
    end

    defp generic_device_class_mappings(doc) do
      doc
      |> xpath(~x"//zw_classes/gen_dev"l, key: ~x"@key"s, name: ~x"@name"s)
      |> Enum.map(key_name_mapper("GENERIC_TYPE_"))
      |> Enum.sort()
    end

    defp specific_device_class_mappings(doc) do
      doc
      |> xpath(~x"//zw_classes/gen_dev"l,
        generic_type: ~x"@name"s,
        specific_types: [~x"./spec_dev"l, key: ~x"@key"s, name: ~x"@name"s]
      )
      |> Enum.flat_map(fn %{generic_type: generic_type, specific_types: specific_types} ->
        Enum.map(specific_types, fn %{name: name, key: key} ->
          generic_type = parse_name(generic_type, "GENERIC_TYPE_")
          specific_type = parse_name(name, "SPECIFIC_TYPE_")
          key = parse_hex_string(key)
          Macro.escape({generic_type, key, specific_type})
        end)
      end)
      |> Enum.sort()
    end

    defp key_name_mapper(name_prefix, name_overrides \\ %{}) do
      fn %{name: name, key: key} ->
        key = parse_hex_string(key)
        name = Map.get(name_overrides, key, parse_name(name, name_prefix))
        {key, name}
      end
    end

    defp parse_hex_string(string) do
      string |> String.replace_leading("0x", "") |> String.to_integer(16)
    end

    defp parse_name(name, prefix) do
      name |> String.replace(prefix, "") |> Macro.underscore() |> String.to_atom()
    end
  end
end
