# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Mix.Tasks.Pleroma.Emoji do
  use Mix.Task

  @shortdoc "Manages emoji packs"
  @moduledoc """
  Manages emoji packs

  ## ls-packs

      mix pleroma.emoji ls-packs [OPTION...]

  Lists the emoji packs and metadata specified in the manifest.

  ### Options

  - `-m, --manifest PATH/URL` - path to a custom manifest, it can
    either be an URL starting with `http`, in that case the
    manifest will be fetched from that address, or a local path

  ## get-packs

      mix pleroma.emoji get-packs [OPTION...] PACKS

  Fetches, verifies and installs the specified PACKS from the
  manifest into the `STATIC-DIR/emoji/PACK-NAME`

  ### Options

  - `-m, --manifest PATH/URL` - same as ls-packs

  ## gen-pack

      mix pleroma.emoji gen-pack PACK-URL

  Creates a new manifest entry and a file list from the specified
  remote pack file. Currently, only .zip archives are recognized
  as remote pack files and packs are therefore assumed to be zip
  archives. This command is intended to run interactively and will
  first ask you some basic questions about the pack, then download
  the remote file and generate an SHA256 checksum for it, then
  generate an emoji file list for you.

  The manifest entry will either be written to a newly created
  `index.json` file or appended to the existing one, *replacing*
  the old pack with the same name if it was in the file previously.

  The file list will be written to the file specified previously,
  *replacing* that file. You _should_ check that the file list doesn't
  contain anything you don't need in the pack, that is, anything that is
  not an emoji (the whole pack is downloaded, but only emoji files
  are extracted).
  """

  @default_manifest Pleroma.Config.get!([:emoji, :default_manifest])

  def run(["ls-packs" | args]) do
    Application.ensure_all_started(:hackney)

    {options, [], []} = parse_global_opts(args)

    manifest =
      fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest)

    Enum.each(manifest, fn {name, info} ->
      to_print = [
        {"Name", name},
        {"Homepage", info["homepage"]},
        {"Description", info["description"]},
        {"License", info["license"]},
        {"Source", info["src"]}
      ]

      for {param, value} <- to_print do
        IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
      end

      # A newline
      IO.puts("")
    end)
  end

  def run(["get-packs" | args]) do
    Application.ensure_all_started(:hackney)

    {options, pack_names, []} = parse_global_opts(args)

    manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest

    manifest = fetch_manifest(manifest_url)

    for pack_name <- pack_names do
      if Map.has_key?(manifest, pack_name) do
        pack = manifest[pack_name]
        src_url = pack["src"]

        IO.puts(
          IO.ANSI.format([
            "Downloading ",
            :bright,
            pack_name,
            :normal,
            " from ",
            :underline,
            src_url
          ])
        )

        binary_archive = Tesla.get!(client(), src_url).body
        archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()

        sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]

        if archive_sha == String.upcase(pack["src_sha256"]) do
          IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
        else
          IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))

          raise "Bad SHA256 for #{pack_name}"
        end

        # The url specified in files should be in the same directory
        files_url = Path.join(Path.dirname(manifest_url), pack["files"])

        IO.puts(
          IO.ANSI.format([
            "Fetching the file list for ",
            :bright,
            pack_name,
            :normal,
            " from ",
            :underline,
            files_url
          ])
        )

        files = Tesla.get!(client(), files_url).body |> Jason.decode!()

        IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))

        pack_path =
          Path.join([
            Pleroma.Config.get!([:instance, :static_dir]),
            "emoji",
            pack_name
          ])

        files_to_unzip =
          Enum.map(
            files,
            fn {_, f} -> to_charlist(f) end
          )

        {:ok, _} =
          :zip.unzip(binary_archive,
            cwd: pack_path,
            file_list: files_to_unzip
          )

        IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))

        emoji_txt_str =
          Enum.map(
            files,
            fn {shortcode, path} ->
              emojo_path = Path.join("/emoji/#{pack_name}", path)
              "#{shortcode}, #{emojo_path}"
            end
          )
          |> Enum.join("\n")

        File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
      else
        IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
      end
    end
  end

  def run(["gen-pack", src]) do
    Application.ensure_all_started(:hackney)

    proposed_name = Path.basename(src) |> Path.rootname()
    name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
    # If there's no name, use the default one
    name = if String.length(name) > 0, do: name, else: proposed_name

    license = String.trim(IO.gets("License: "))
    homepage = String.trim(IO.gets("Homepage: "))
    description = String.trim(IO.gets("Description: "))

    proposed_files_name = "#{name}.json"
    files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
    files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name

    default_exts = [".png", ".gif"]
    default_exts_str = Enum.join(default_exts, " ")

    exts =
      String.trim(
        IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
      )

    exts =
      if String.length(exts) > 0 do
        String.split(exts, " ")
        |> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
      else
        default_exts
      end

    IO.puts("Downloading the pack and generating SHA256")

    binary_archive = Tesla.get!(client(), src).body
    archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()

    IO.puts("SHA256 is #{archive_sha}")

    pack_json = %{
      name => %{
        license: license,
        homepage: homepage,
        description: description,
        src: src,
        src_sha256: archive_sha,
        files: files_name
      }
    }

    tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")

    {:ok, _} =
      :zip.unzip(
        binary_archive,
        cwd: tmp_pack_dir
      )

    emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)

    File.write!(files_name, Jason.encode!(emoji_map, pretty: true))

    IO.puts("""

    #{files_name} has been created and contains the list of all found emojis in the pack.
    Please review the files in the remove those not needed.
    """)

    if File.exists?("index.json") do
      existing_data = File.read!("index.json") |> Jason.decode!()

      File.write!(
        "index.json",
        Jason.encode!(
          Map.merge(
            existing_data,
            pack_json
          ),
          pretty: true
        )
      )

      IO.puts("index.json file has been update with the #{name} pack")
    else
      File.write!("index.json", Jason.encode!(pack_json, pretty: true))

      IO.puts("index.json has been created with the #{name} pack")
    end
  end

  defp fetch_manifest(from) do
    Jason.decode!(
      if String.starts_with?(from, "http") do
        Tesla.get!(client(), from).body
      else
        File.read!(from)
      end
    )
  end

  defp parse_global_opts(args) do
    OptionParser.parse(
      args,
      strict: [
        manifest: :string
      ],
      aliases: [
        m: :manifest
      ]
    )
  end

  defp client do
    middleware = [
      {Tesla.Middleware.FollowRedirects, [max_redirects: 3]}
    ]

    Tesla.client(middleware)
  end
end