defmodule Releaser.VersionUtils do
  @doc """
  Some utilities to get and set version numbers in the `mix.exs` file and to programmatically transform version numbers.

  1. Create a file RELEASE.md and fill out the following templates.

  ```
  RELEASE_TYPE: patch

  - Fixed x
  - Added y
  ```

  Release types:
  - major: 0.0.1 -> 1.0.0 (when you make incompatible API changes)
  - minor: 0.0.1 -> 0.1.0 (when you add functionality in a backwards compatible manner)
  - patch: 0.0.1 -> 0.0.2 (when you make backwards compatible bug fixes)

  2. Run in the terminal:

        mix run priv/release.exs

  3. Deploy to Hex.pm

        mix hex.publish

  4. Push to git

        git push origin main
        git push --tags

  5. Create release on Github https://github.com/petalframework/petal_components/releases/new

  6. Announce (optional):

  - Tweet
  - Elixir forum post: https://elixirforum.com/t/petal-components-package-a-set-of-tailwind-styled-components-written-in-heex/43633/63
  - Reddit: https://reddit.com/r/elixir
  """
  @version_line_regex ~r/(\n\s*@version\s+")([^\n]+)("\n)/

  def bump_major(%Version{} = version) do
    %{version | major: version.major + 1, minor: 0, patch: 0}
  end

  def bump_minor(%Version{} = version) do
    %{version | minor: version.minor + 1, patch: 0}
  end

  def bump_patch(%Version{} = version) do
    %{version | patch: version.patch + 1}
  end

  def version_to_string(%Version{} = version) do
    "#{version.major}.#{version.minor}.#{version.patch}"
  end

  def get_version() do
    config = File.read!("mix.exs")

    case Regex.run(@version_line_regex, config) do
      [_line, _pre, version, _post] ->
        Version.parse!(version)

      _ ->
        raise "Invalid project version in your mix.exs file"
    end
  end

  def set_version(old_version, version) do
    version_string = version_to_string(version)
    old_version_string = version_to_string(old_version)
    contents = File.read!("mix.exs")

    replaced =
      Regex.replace(@version_line_regex, contents, fn _, pre, _version, post ->
        "#{pre}#{version_string}#{post}"
      end)

    File.write!("mix.exs", replaced)

    # Update the readme to point to the latest version
    readme_contents = File.read!("README.md")
    replaced_readme = String.replace(readme_contents, old_version_string, version_string)
    File.write!("README.md", replaced_readme)
  end

  def update_version(%Version{} = version, "major"), do: bump_major(version)
  def update_version(%Version{} = version, "minor"), do: bump_minor(version)
  def update_version(%Version{} = version, "patch"), do: bump_patch(version)
  def update_version(%Version{} = _version, type), do: raise("Invalid version type: #{type}")
end

defmodule Releaser.Changelog do
  @doc """
  Functions to append entries to the changelog.
  """
  alias Releaser.VersionUtils

  @release_filename "RELEASE.md"
  @release_type_regex ~r/^(RELEASE_TYPE:\s+)(\w+)(.*)/s

  @changelog_filename "CHANGELOG.md"
  @changelog_entry_header_level 3
  @changelog_entries_marker "# Changelog\n"

  def remove_release_file() do
    File.rm!(@release_filename)
  end

  def extract_release_type() do
    contents = File.read!(@release_filename)

    {type, text} =
      case Regex.run(@release_type_regex, contents) do
        [_line, _pre, type, text] ->
          {type, String.trim(text)}

        _ ->
          raise "Invalid project version in your mix.exs file"
      end

    {type, text}
  end

  def changelog_entry(%Version{} = version, %DateTime{} = date_time, text) do
    header_prefix = String.duplicate("#", @changelog_entry_header_level)
    version_string = VersionUtils.version_to_string(version)

    date_time_string =
      date_time
      |> DateTime.truncate(:second)
      |> NaiveDateTime.to_string()

    """
    #{header_prefix} #{version_string} - #{date_time_string}

    #{text}

    """
  end

  def add_changelog_entry(entry) do
    contents = File.read!(@changelog_filename)
    [first, last] = String.split(contents, @changelog_entries_marker)

    replaced =
      Enum.join([
        first,
        @changelog_entries_marker,
        entry,
        last
      ])

    File.write!(@changelog_filename, replaced)
  end
end

defmodule Releaser.Git do
  @doc """
  This module contains some git-specific functionality
  """
  alias Releaser.VersionUtils

  def add_commit_and_tag(version) do
    version_string = VersionUtils.version_to_string(version)
    Mix.Shell.IO.cmd("git add .", [])
    Mix.Shell.IO.cmd(~s'git commit -m "v#{version_string}"')
    Mix.Shell.IO.cmd(~s'git tag -a v#{version_string} -m "Version #{version_string}"')
  end
end

defmodule Releaser.Tests do
  def run_tests!() do
    error_code = Mix.Shell.IO.cmd("mix test", [])

    if error_code != 0 do
      raise "This version can't be released because tests are failing."
    end

    :ok
  end
end

defmodule Releaser.Publish do
  def publish!() do
    error_code = Mix.Shell.IO.cmd("mix hex.publish", [])

    if error_code != 0 do
      raise "Couldn't publish package on hex."
    end

    :ok
  end
end

defmodule Releaser do
  alias Releaser.VersionUtils
  alias Releaser.Changelog
  alias Releaser.Git
  alias Releaser.Tests
  alias Releaser.Publish

  def run() do
    # Run the tests before generating the release.
    # If any test fails, stop.
    Tests.run_tests!()

    # Get the current version from the mix.exs file.
    version = VersionUtils.get_version()

    # Extract the changelog entry and add it to the changelog.
    # Use the information in the RELEASE.md file to bump the version number.
    {release_type, text} = Changelog.extract_release_type()
    new_version = VersionUtils.update_version(version, release_type)
    entry = Changelog.changelog_entry(new_version, DateTime.utc_now(), text)
    Changelog.add_changelog_entry(entry)

    # Set a new version on the mix.exs file
    VersionUtils.set_version(version, new_version)

    # Remove the release file
    Changelog.remove_release_file()

    # Commit the changes and ad a new 'v*.*.*' tag
    Git.add_commit_and_tag(new_version)
  end
end

# Generate and publish a new release
Releaser.run()
