defmodule Mix.Tasks.MultiSelect.Install do
  @moduledoc """
  Installs multi_select component in the existing project.

  ```bash
  $ mix multi_select.install
  ```
  """

  @shortdoc "Installs MultiSelect component"
  use Mix.Task

  @impl true
  def run(_args) do
    modify_tailwind_cfg()
    modify_npm_cfg()
    install_js_hook()
    check_alpinejs()
    :ok
  end

  defp modify_tailwind_cfg() do
    file = "assets/tailwind.config.js"

    if not File.exists?(file) do
      File.write!(file, new_tailwind_cfg())
      IO.puts("==> File #{file} created")
    else
      tailwind_cfg = File.read!(file)
      ## Add the "../deps/phoenix_multi_select/lib/*.ex" string to the `content` section
      str =
        if tailwind_cfg =~ content_mask() do
          tailwind_cfg
        else
          {str, module_exports_offset} =
            case Regex.run(~r/^module\.exports\s*=\s*\{\n*/m, tailwind_cfg, return: :index) do
              [offset] ->
                {tailwind_cfg, offset}
              nil ->
                raise RuntimeError, message: "Cannot find 'module.exports = {...}' in #{file}"
            end
          case Regex.run(~r/\n(\s*)content:\s*\[([^\]]+)/, str, return: :index) do
            [{s1n, s1m}, {_, indent_wid}, {s2n, s2m}] ->
              {s1, _} = String.split_at(str, s1n+s1m-s2m)
              {_, s3} = String.split_at(str, s2n+s2m)
              content =
                str
                |> String.slice(s2n, s2m)
                |> then(& Regex.replace(~r/[ ,\n]+$/, &1, ""))
                |> then(& &1 == "" && &1 || &1 <> ",")
                |> then(& """
                    #{&1}
                    #{String.duplicate(" ", indent_wid*2)}#{content_mask()},
                    """)
              s1 <> content <> String.duplicate(" ", indent_wid) <> s3
            _ ->
              {s1, s2} = String.split_at(str, elem(module_exports_offset, 0)+elem(module_exports_offset, 1))
              s1 <> content_str() <> s2
          end
        end
      ## Add the `primary` custom color
      str =
        if str =~ ~r/colors:\s*\{[^p]+primary:\s*[^,]+,/ do
          str
        else
          {str, file}
          |> add_theme()
          |> add_extend()
          |> add_colors()
          |> add_primary()
        end

      if str == tailwind_cfg do
        out("==> File #{file} doesn't require modifications")
      else
        File.write!(file, str)
        IO.puts("==> Added definition of primary color to #{file}")
      end
    end
  end

  defp content_str() do
    """
      content: [
        "./js/**/*.js",
        "../lib/*_web.ex",
        "../lib/*_web/**/*.*ex",
        #{content_mask()}
      ],
    """
  end

  defp content_mask(), do: "\"../deps/phoenix_multi_select/lib/*.ex\""

  defp modify_npm_cfg() do
    file = "assets/package.json"
    if File.exists?(file) and (File.read!(file) =~ "tailwind-scrollbar") do
      out("==> File #{file} doesn't require modifications")
    else
      pkg_mgr = get_package_mgr(["npm", "yarn"])
      {_, 0} = System.cmd(pkg_mgr, ~w(install -D tailwind-scrollbar), cd: "assets")
      IO.puts("==> Added tailwind-scrollbar NPM dev package")
    end
  end

  defp get_package_mgr([]) do
    raise RuntimeError, message: "No JS package manager found: npm, yarn."
  end

  defp get_package_mgr([name|tail]) do
    case System.cmd("sh", ["-c", "which " <> name]) do
      {_, 0} -> name
      {_, _} -> get_package_mgr(tail)
    end
  end

  defp install_js_hook() do
    hooks_folder = "assets/js/hooks"
    index_file   = "assets/js/hooks/index.js"

    File.exists?(hooks_folder) || File.mkdir!(hooks_folder)
    File.exists?(index_file)   || File.touch!(index_file)

    file = "multi-select-hook.js"
    path = "assets/js/hooks/#{file}"
    src  = "#{Mix.Project.deps_paths()[:phoenix_multi_select]}/assets/#{file}"
    if File.exists?(path) && File.read!(path) == File.read!(src) do
      out("==> File #{path} is already installed")
    else
      File.cp!(src, path)
      IO.puts("==> Installed #{path} file")
    end

    index = "assets/js/hooks/index.js"
    if File.exists?(index) do
      f = File.read!(index)
      str =
        if not (f =~ "import MultiSelectHook") do
          "import MultiSelectHook from \"./multi-select-hook\"\n" <> f
        else
          f
        end
      str =
        case Regex.run(~r/\n\s*export[^{]+{/, str, return: :index) do
          [{n, m}] ->
            {s1, s2} = String.split_at(str, n+m)
            if s2 =~ "MultiSelectHook" do
              str
            else
              s2 = String.trim_leading(s2, "\n")
              s1 <> "\n  MultiSelectHook,\n" <> s2
            end
          _ ->
            str <> "\nexport default {\n  MultiSelectHook\n}\n"
        end

      if f != str do
        File.write!(index, str)
        IO.puts("==> File #{index} modified")
      else
        out("==> File #{index} doesn't require modifications")
      end
    end
  end

  defp check_alpinejs() do
    if Application.get_env(:phoenix_multi_select, :use_alpinejs) do
      found =
        case Path.wildcard("lib/**/root.html.heex") do
          [root_html] -> File.read!(root_html) =~ ~r/<[^s]*script.+alpinejs/
          []          -> false
        end
      file  = "assets/js/app.js"
      found = found ||
        case File.exists?(file) do
          true  -> File.read!(file) =~ ~r/import.+alpinejs/
          false -> false
        end
      if not found do
        IO.puts("""
          ==> Configuration option `config :phoenix_multi_select, use_alpinejs: true`
          ==> requires that AlpineJS is available.
          ==>
          ==> However AlpineJS configuration is missing! You either need to include:
          ==>
          ==>   <script src="//unpkg.com/alpinejs" defer></script>
          ==>
          ==> in the root.html.heex template, or add this to the assets/js/app.js:
          ==>
          ==>   import 'alpinejs'
          ==>
          ==>   window.Alpine = Alpine
          ==>   Alpine.start()
          """)
      end
    end
  end

  defp add_theme({str, file}) do
    case Regex.run(~r/\n(\s*)theme:\s*\{/, str, return: :index) do
      [theme_idx, indent_idx] ->
        {str, file, indent_idx, theme_idx}
      _ ->
        raise RuntimeError, message: "Cannot find 'theme:' entry in #{file}"
    end
  end

  defp add_extend({str, file, {_, indent_wid} = indent_idx, {n, m} = _theme_idx}) do
    case Regex.run(~r/\n(\s*)extend:\s*\{/, str, return: :index) do
      [idx, indent_idx] ->
        {str, file, indent_idx, idx}
      _ ->
        {s1, s2} = String.split_at(str, n+m)
        indent   = String.duplicate(" ", indent_wid)
        res1     = s1 <> indent <> "extend: {\n"
        res2     = indent <> "},\n" <> s2
        {res1 <> res2, file, indent_idx, {0, byte_size(res1)}}
    end
  end

  defp add_colors({str, file, {_, indent_wid} = indent_idx, {n, m} = _extend_idx}) do
    case Regex.run(~r/\n(\s*)colors:\s*\{/, str, return: :index) do
      [idx, indent_idx] ->
        {str, file, indent_idx, idx}
      _ ->
        {s1, s2} = String.split_at(str, n+m)
        indent   = String.duplicate(" ", indent_wid)
        res1     = s1 <> String.duplicate(indent, 2) <> "colors: {\n"
        res2     = indent <> "},\n" <> s2
        {res1 <> res2, file, indent_idx, {0, byte_size(res1)}}
    end
  end

  defp add_primary({str, _file, {_, indent_wid}, {n, m} = _colors_idx}) do
    case Regex.run(~r/\n(\s*)primary:\s*[^,]+,/, str, return: :index) do
      [_idx, _indent_idx] ->
        str
      _ ->
        {s1, s2} = String.split_at(str, n+m)
        indent   = String.duplicate(" ", indent_wid)
        res      = s1 <> "\n" <> String.duplicate(indent, 3) <> "primary: colors.blue,\n"
                      <> String.trim_leading(s2, "\n")
        if (res =~ "require(\"tailwindcss/colors\")") do
          res
        else
          [{offset, _}|_] = Regex.run(~r/module.exports/, res, return: :index)
          {s1, s2} = String.split_at(res, offset)
          res = s1 <> "const colors = require(\"tailwindcss/colors\")\n" <> s2
          res
        end
    end
  end

  defp new_tailwind_cfg() do
    """
    const plugin = require("tailwindcss/plugin")
    const colors = require("tailwindcss/colors")

    module.exports = {
      content: [
        "./js/**/*.js",
        "../lib/*_web.ex",
        "../lib/*_web/**/*.*ex",
        #{content_mask()},
      ],
      theme: {
        extend: {
          colors: {
            primary: colors.blue,
          }
        }
      },
      darkMode: "class",
      plugins: [
        require("@tailwindcss/forms"),
        require('tailwind-scrollbar'),
      ]
    }
    """
  end

  defp out(str), do:
    (System.get_env("DEBUG", "0") |> String.to_integer() > 0) && IO.puts(str)
end
