lib/vix/vips/image.ex

defmodule Vix.Vips.Image do
  defstruct [:ref]

  alias __MODULE__

  @moduledoc """
  Functions for reading and writing images as well as
  accessing and updating image metadata.

  ## Access syntax (slicing)

  Vix images implement Elixir's access syntax. This allows developers
  to slice images and easily access sub-dimensions and values.

  ### Integer
  Access accepts integers. Integers will extract an image band using parameter as index:

      #=> {:ok, i} = Image.new_from_file("./test/images/puppies.jpg")
      {:ok, %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153539>}}
      #=> i[0]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153540>}

  If a negative index is given, it accesses the band from the back:

      #=> i[-1]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153540>}

  Out of bound access will throw an `ArgumentError` exception:

      #=> i[-4]
      ** (ArgumentError) Invalid band requested. Found -4

  ### Range

  Access also accepts ranges. Ranges in Elixir are inclusive:

      #=> i[0..1]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153641>}

  Ranges can receive negative positions and they will read from
  the back. In such cases, the range step must be explicitly given
  (on Elixir 1.12 and later) and the right-side of the range must
  be equal or greater than the left-side:

      #=> i[0..-1//1]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153703>}

  To slice across multiple dimensions, you can wrap the ranges in a list.
  The list will be of the form `[with_slice, height_slice, band_slice]`.

      # Returns an image that slices a 10x10 pixel square
      # from the top left of the image with three bands
      #=> i[[0..9, 0..9, 0..3]]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153738>}

  If number of dimensions are less than 3 then remaining dimensions
  are returned in full

      # If `i` size is 100x100 with 3 bands
      #=> i[[0..9, 0..9]] # equivalent to `i[[0..9, 0..9, 0..2]]`
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153740>}

      #=> i[[0..9]] # equivalent to `i[[0..9, 0..99, 0..2]]`
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153703>}

  Slices can include negative ranges in which case the indexes
  are calculated from the right and bottom of the image.

      # Slices the bottom right 10x10 pixels of the image
      # and returns all bands.
      #=> i[[-10..-1, -10..-1]]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153742>}

  Slice can mix integers and ranges

      # Slices the bottom right 10x1 pixels of the image
      # and returns all bands.
      #=> i[[-10..-1, -1]]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153742>}

  ### Keyword List

  Access also accepts keyword list. Where key can be any of `width`,
  `height`, `band`.  and value must be an `integer`, `range`. This is
  useful for complex scenarios when you want omit dimensions arbitrary
  dimensions.

      # Slices an image with height 10 with max width and all bands
      #=> i[[height: 0..10]]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153742>}

      # Slices an image with single band 1
      #=> i[[band: 1]]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153742>}

      # Slices the bottom right 10x10 pixels of the image
      # and returns all bands.
      #=> i[[width: -10..-1, height: -10..-1]]
      %Vix.Vips.Image{ref: #Reference<0.2448791511.2685009949.153742>}
  """

  alias Vix.Nif
  alias Vix.Type
  alias Vix.Vips.MutableImage
  alias Vix.Vips.Operation

  require Logger

  defmodule Error do
    defexception [:message]
  end

  @behaviour Type

  @typedoc """
  Represents an instance of VipsImage
  """
  @type t() :: %Image{ref: reference()}

  @impl Type
  def typespec do
    quote do
      unquote(__MODULE__).t()
    end
  end

  @impl Type
  def default(nil), do: :unsupported

  @impl Type
  def to_nif_term(image, _data) do
    case image do
      %Image{ref: ref} ->
        ref

      value ->
        raise ArgumentError, message: "expected Vix.Vips.Image. given: #{inspect(value)}"
    end
  end

  @impl Type
  def to_erl_term(ref), do: %Image{ref: ref}

  # Implements the Access behaviour for Vix.Vips.Image to allow
  # access to image bands. For example `image[1]`. Note that
  # due to the nature of images, `pop/2` and `put_and_update/3`
  # are not supported.

  @behaviour Access

  @impl Access

  # Extract band when the band number is positive or zero
  def fetch(image, band) when is_integer(band) and band >= 0 do
    case Vix.Vips.Operation.extract_band(image, band) do
      {:ok, band} -> {:ok, band}
      {:error, _reason} -> raise ArgumentError, "Invalid band requested. Found #{inspect(band)}"
    end
  end

  # Extract band when the band number is negative
  def fetch(image, band) when is_integer(band) and band < 0 do
    case bands(image) + band do
      band when band >= 0 -> fetch(image, band)
      _other -> raise ArgumentError, "Invalid band requested. Found #{inspect(band)}"
    end
  end

  def fetch(image, %Range{} = range) do
    if single_step_range?(range) do
      fetch_range(image, range)
    else
      raise ArgumentError, "Range arguments must have a step of 1. Found #{inspect(range)}"
    end
  end

  # Slicing the image
  def fetch(image, args) when is_list(args) do
    with {:ok, args} <- normalize_access_args(args),
         {:ok, left, width} <- validate_dimension(args[:width], width(image)),
         {:ok, top, height} <- validate_dimension(args[:height], height(image)),
         {:ok, first_band, bands} <- validate_dimension(args[:band], bands(image)),
         {:ok, area} <- extract_area(image, left, top, width, height) do
      extract_band(area, first_band, n: bands)
    else
      {:error, _} ->
        raise ArgumentError, "Argument must be list of integers or ranges or keyword list"
    end
  end

  @impl Access
  def get_and_update(_image, _key, _fun) do
    raise "get_and_update/3 for Vix.Vips.Image is not supported."
  end

  @impl Access
  def pop(_image, _band, _default \\ nil) do
    raise "pop/3 for Vix.Vips.Image is not supported."
  end

  # Extract a range of bands
  def fetch_range(image, %Range{first: first, last: last}) when first >= 0 and last >= first do
    case Vix.Vips.Operation.extract_band(image, first, n: last - first + 1) do
      {:ok, band} -> {:ok, band}
      {:error, _reason} -> raise "Invalid band range #{inspect(first..last)}"
    end
  end

  def fetch_range(image, %Range{first: first, last: last}) when first >= 0 and last < 0 do
    case bands(image) + last do
      last when last >= 0 -> fetch(image, first..last)
      _other -> raise ArgumentError, "Resolved invalid band range #{first..last}}"
    end
  end

  def fetch_range(image, %Range{first: first, last: last}) when last < 0 and first < last do
    bands = bands(image)
    last = bands + last

    if last > 0 do
      fetch(image, (bands + first)..last)
    else
      raise ArgumentError, "Resolved invalid range #{(bands + first)..last}"
    end
  end

  def fetch_range(_image, %Range{} = range) do
    raise ArgumentError, "Invalid range #{inspect(range)}"
  end

  # For nil use maximum value
  defp validate_dimension(nil, limit), do: {:ok, 0, limit}

  # For integer treat it as single band
  defp validate_dimension(index, limit) when is_integer(index) do
    index = if index < 0, do: limit + index, else: index

    if index < limit do
      {:ok, index, 1}
    else
      raise ArgumentError,
            "Invalid dimension #{inspect(index)}. Dimension must be between 0 and #{inspect(index - 1)}"
    end
  end

  # For positive ranges start from the left and top
  defp validate_dimension(%Range{} = range, width) do
    if single_step_range?(range) do
      validate_range_dimension(range, width)
    else
      raise ArgumentError, "Range arguments must have a step of 1. Found #{inspect(range)}"
    end
  end

  # For positive ranges start from the left and top
  defp validate_range_dimension(%Range{first: first, last: last}, width)
       when first >= 0 and last > first and last < width do
    {:ok, first, last - first + 1}
  end

  # For negative ranges start from the right and bottom
  defp validate_range_dimension(%Range{first: first, last: last}, limit)
       when first < 0 and last < 0 and last > first and abs(first) < limit do
    {:ok, limit + first, limit + last - (limit + first) + 1}
  end

  # Positive start to a negative end
  defp validate_range_dimension(%Range{first: first, last: last}, limit)
       when first >= 0 and last < 0 and abs(last) <= limit do
    {:ok, first, limit + last - first + 1}
  end

  defp validate_range_dimension(range, _limit) do
    raise ArgumentError, "Invalid range #{inspect(range)}"
  end

  # We can do this as a guard in later Elixir versions but
  # Vix is intendede to run on a wide range of Elixir versions.

  defp single_step_range?(%Range{} = range) do
    Map.get(range, :step) == 1 || !Map.has_key?(range, :step)
  end

  defp extract_area(image, left, top, width, height) do
    case Operation.extract_area(image, left, top, width, height) do
      {:ok, image} -> {:ok, image}
      _other -> raise "Requested area could not be extracted"
    end
  end

  defp extract_band(area, first_band, options) do
    case Operation.extract_band(area, first_band, options) do
      {:ok, image} -> {:ok, image}
      _other -> raise "Requested band(s) could not be extracted"
    end
  end

  @doc """
  Opens `path` for reading, returns an instance of `t:Vix.Vips.Image.t/0`

  It can load files in many image formats, including VIPS, TIFF, PNG,
  JPEG, FITS, Matlab, OpenEXR, CSV, WebP, Radiance, RAW, PPM and
  others.

  Load options may be appended to filename as "[name=value,...]". For
  example:

  ```elixir
  Image.new_from_file("fred.jpg[shrink=2]")
  ```
  Will open "fred.jpg", downsampling by a factor of two.

  The full set of options available depend upon the load operation
  that will be executed. Try something like:

  ```shell
  $ vips jpegload
  ```

  at the command-line to see a summary of the available options for
  the JPEG loader.

  If you want more control over the loader, Use specific format loader
  from `Vix.Vips.Operation`. For example for jpeg use
  `Vix.Vips.Operation.jpegload/2`

  Loading is fast: only enough of the image is loaded to be able to
  fill out the header. Pixels will only be decompressed when they are
  needed.
  """
  @spec new_from_file(String.t()) :: {:ok, __MODULE__.t()} | {:error, term()}
  def new_from_file(path) do
    path = Path.expand(path)

    Nif.nif_image_new_from_file(normalize_string(path))
    |> wrap_type()
  end

  @doc """
  Create a new image based on an existing image with each pixel set to `value`

  Creates a new image with width, height, format, interpretation,
  resolution and offset taken from the input image, but with each band
  set from `value`.
  """
  @spec new_from_image(__MODULE__.t(), [float()]) :: {:ok, __MODULE__.t()} | {:error, term()}
  def new_from_image(%Image{ref: vips_image}, value) do
    float_value = Enum.map(value, &Vix.GObject.Double.normalize/1)

    Nif.nif_image_new_from_image(vips_image, float_value)
    |> wrap_type()
  end

  @doc """
  Create a new image from formatted binary

  Create a new image from formatted binary `bin`. Binary should be an
  image encoded in a format such as JPEG. It tries to recognize the
  format by checking the binary.

  If you already know the image format of `bin` then you can just use
  corresponding loader operation function directly from
  `Vix.Vips.Operation` instead. For example to load jpeg, you can use
  `Vix.Vips.Operation.jpegload_buffer/2`

  `bin` should be formatted binary (ie. JPEG, PNG etc). For loading
  unformatted binary (raw pixel data) see `new_from_binary/5`.

  Optional param `opts` is passed to the image loader. Options
  available depend on the file format. You can find all options
  available like this:

  ```sh
  $ vips jpegload_buffer
  ```

  Not all loaders support load from buffer, but at least JPEG, PNG and
  TIFF images will work.
  """
  @spec new_from_buffer(binary(), keyword()) :: {:ok, __MODULE__.t()} | {:error, term()}
  def new_from_buffer(bin, opts \\ []) do
    with {:ok, loader} <- Vix.Vips.Foreign.find_load_buffer(bin),
         {:ok, {ref, _optional}} <- Vix.Vips.OperationHelper.operation_call(loader, [bin], opts) do
      {:ok, wrap_type(ref)}
    end
  end

  @doc """
  Create a new image from raw pixel data

  Creates an image by wrapping passed raw pixel data. This function
  does not copy the passed binary, instead it just creates a reference
  to the binary term (zero-copy). So this function is very efficient.

  This function is useful when you are getting raw pixel data from
  some other library like
  [`eVision`](https://github.com/cocoa-xu/evision) or
  [`Nx`](https://github.com/elixir-nx/) and want to perform some
  operation on it using Vix.

  Binary should be sequence of pixel data, for example: RGBRGBRGB. and
  order should be left-to-right, top-to-bottom.

  `bands` should be number values which represent the each pixel. For
  example: if each pixel is RGB then `bands` will be 3. If each pixel
  is RGBA then `bands` will be 4.  `band_format` refers to type of
  each band. Usually it will be `:VIPS_FORMAT_UCHAR`.

  `bin` should be raw pixel data binary. For loading
  formatted binary (JPEG, PNG) see `new_from_buffer/2`.

  ##  Endianness

  Byte order of the data *must* be in native endianness. This matters
  if you are generating or manipulating binary by using bitstring
  syntax. By default bitstring treat binary byte order as `big` endian
  which might *not* be native. Always use `native` specifier to
  ensure. See [Elixir
  docs](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1-endianness)
  for more details.

  """
  @spec new_from_binary(
          binary(),
          pos_integer(),
          pos_integer(),
          pos_integer(),
          Vix.Vips.Operation.vips_band_format()
        ) :: {:ok, __MODULE__.t()} | {:error, term()}
  def new_from_binary(bin, width, height, bands, band_format)
      when width > 0 and height > 0 and bands > 0 do
    band_format = Vix.Vips.Enum.VipsBandFormat.to_nif_term(band_format, nil)

    Nif.nif_image_new_from_binary(bin, width, height, bands, band_format)
    |> wrap_type()
  end

  @doc """
  Create a new image from Enumerable.

  > #### Caution {: .warning}
  > This function is experimental and might cause crashes, use with caution

  Returns an image which will lazily pull data from passed
  Enumerable. `enum` should be stream of bytes of an encoded image
  such as JPEG. This functions recognizes the image format and
  metadata by reading starting bytes and wraps passed Enumerable as an
  image. Remaining bytes are read on-demand.

  Useful when working with big images. Where you don't want to load
  complete input image data to memory.

  ```elixir
  {:ok, image} =
    File.stream!("puppies.jpg", [], 1024) # or read from s3, web-request
    |> Image.new_from_enum()

  :ok = Image.write_to_file(image, "puppies.png")
  ```

  Optional param `opts` string is passed to the image loader. It is a string
  of the format "[name=value,...]".

  ```elixir
  Image.new_from_enum(stream, "[shrink=2]")
  ```

  Will read the stream with downsampling by a factor of two.

  The full set of options available depend upon the image format. You
  can find all options available at the command-line. To see a summary
  of the available options for the JPEG loader:

  ```shell
  $ vips jpegload_source
  ```

  """
  @spec new_from_enum(Enumerable.t(), String.t()) :: {:ok, __MODULE__.t()} | {:error, term()}
  def new_from_enum(enum, opts \\ "") do
    parent = self()

    pid =
      spawn_link(fn ->
        {pipe, source} = Vix.SourcePipe.new()
        send(parent, {self(), source})

        Enum.each(enum, fn iodata ->
          bin =
            try do
              IO.iodata_to_binary(iodata)
            rescue
              ArgumentError ->
                Logger.warn("argument must be stream of iodata")
                Vix.SourcePipe.stop(pipe)
                exit(:normal)
            end

          :ok = Vix.SourcePipe.write(pipe, bin)
        end)

        Vix.SourcePipe.stop(pipe)
      end)

    receive do
      {^pid, source} ->
        Nif.nif_image_new_from_source(source, opts)
        |> wrap_type()
    end
  end

  @doc """
  Creates a Stream from Image.

  > #### Caution {: .warning}
  > This function is experimental and might cause crashes, use with caution

  Returns a Stream which will lazily pull data from passed image.

  Useful when working with big images. Where you don't want to keep
  complete output image in memory.


  ```elixir
  {:ok, image} = Image.new_from_file("puppies.jpg")

  :ok =
    Image.write_to_stream(image, ".png")
    |> Stream.into(File.stream!("puppies.png")) # or write to S3, web-request
    |> Stream.run()
  ```

  Second param `suffix` determines the format of the output
  stream. Save options may be appended to the suffix as
  "[name=value,...]".

  ```elixir
  Image.write_to_stream(vips_image, ".jpg[Q=90]")
  ```

  Options are specific to save operation. You can find out all
  available options for the save operation at command line. For
  example:

  ```shell
  $ vips jpegsave_target
  ```

  """
  @spec write_to_stream(__MODULE__.t(), String.t()) :: Enumerable.t()
  def write_to_stream(%Image{ref: vips_image}, suffix) do
    Stream.resource(
      fn ->
        {:ok, pipe} = Vix.TargetPipe.new(vips_image, suffix)
        pipe
      end,
      fn pipe ->
        ret = Vix.TargetPipe.read(pipe)

        case ret do
          :eof ->
            {:halt, pipe}

          {:ok, bin} ->
            {[bin], pipe}

          {:error, reason} ->
            raise Error, reason
        end
      end,
      fn pipe ->
        Vix.TargetPipe.stop(pipe)
      end
    )
  end

  # Copy an image to a memory area.
  # If image is already a memory buffer, just ref and return. If it's
  # a file on disc or a partial, allocate memory and copy the image to
  # it. Intended to be used with draw operations when they are
  # properly supported
  @doc false
  @spec copy_memory(__MODULE__.t()) :: {:ok, __MODULE__.t()} | {:error, term()}
  def copy_memory(%Image{ref: vips_image}) do
    Nif.nif_image_copy_memory(vips_image)
    |> wrap_type()
  end

  @doc """
  Write `vips_image` to a file.

  Save options may be encoded in the filename. For example:

  ```elixir
  Image.write_to_file(vips_image, "fred.jpg[Q=90]")
  ```

  A saver is selected based on image extension in `path`. The full set
  of save options depend on the selected saver. Try something like:

  ```shell
  $ vips jpegsave
  ```
  at the command-line to see all the available options for JPEG save.

  If you want more control over the saver, Use specific format saver
  from `Vix.Vips.Operation`. For example for jpeg use
  `Vix.Vips.Operation.jpegsave/2`

  """
  @spec write_to_file(__MODULE__.t(), String.t()) :: :ok | {:error, term()}
  def write_to_file(%Image{ref: vips_image}, path) do
    Nif.nif_image_write_to_file(vips_image, normalize_string(Path.expand(path)))
  end

  @doc """
  Returns `vips_image` as binary based on the format specified by `suffix`.

  This function is similar to `write_to_file` but instead of writing
  the output to the file, it returns it as a binary.

  Currently only TIFF, JPEG and PNG formats are supported.

  Save options may be encoded in the filename. For example:

  ```elixir
  Image.write_to_buffer(vips_image, ".jpg[Q=90]")
  ```

  The full set of save options depend on the selected saver. You can
  get list of available options for the saver

  ```shell
  $ vips jpegsave
  ```
  """
  @spec write_to_buffer(__MODULE__.t(), String.t()) ::
          {:ok, binary()} | {:error, term()}
  def write_to_buffer(%Image{ref: vips_image}, suffix) do
    Nif.nif_image_write_to_buffer(vips_image, normalize_string(suffix))
  end

  @doc """
  Returns raw pixel data of the image as `Vix.Tensor`

  VIPS images are three-dimensional arrays, the dimensions being
  width, height and bands.

  Each dimension can be up to 2 ** 31 pixels (or band elements).
  An image has a format, meaning the machine number type used to
  represent each value. VIPS supports 10 formats, from 8-bit unsigned
  integer up to 128-bit double complex.

  In VIPS, images are uninterpreted arrays, meaning that from
  the point of view of most operations, they are just large
  collections of numbers. There's no difference between an RGBA
  (RGB with alpha) image and a CMYK image, for example, they are
  both just four-band images.

  This function is intended to support interoperability of image
  data between different libraries.  Since the array is created as
  a NIF resource it will be correctly garbage collected when
  the last reference falls out of scope.

  Libvips might run all the operations to produce the pixel data
  depending on the caching mechanism and how image is built.

  ##  Endianness

  Returned binary term will be in native endianness. By default
  bitstring treats byte order as `big` endian which might *not* be
  native. Always use `native` specifier to ensure. See [Elixir
  docs](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1-endianness)
  for more details.

  """
  @spec write_to_tensor(__MODULE__.t()) :: {:ok, Vix.Tensor.t()} | {:error, term()}
  def write_to_tensor(%Image{} = image) do
    with {:ok, binary} <- write_to_binary(image) do
      tensor = %Vix.Tensor{
        data: binary,
        shape: {width(image), height(image), bands(image)},
        names: [:width, :height, :bands],
        type: Vix.Tensor.type(image)
      }

      {:ok, tensor}
    end
  end

  @doc """
  Returns raw pixel data of the image as binary term

  Please check `write_to_tensor` for more details. This function just
  returns the data instead of the `Vix.Tensor` struct.

  Prefer using `write_to_tensor` instead of this function. This is
  only useful if you already know the details about the returned
  binary blob. Such as height, width and bands.
  """
  @spec write_to_binary(__MODULE__.t()) :: {:ok, binary()} | {:error, term()}
  def write_to_binary(%Image{ref: vips_image}) do
    Nif.nif_image_write_to_binary(vips_image)
  end

  @doc """
  Make a VipsImage which, when written to, will create a temporary file on disc.

  The file will be automatically deleted when the image is destroyed. format is something like `"%s.v"` for a vips file.

  The file is created in the temporary directory. This is set with the environment variable TMPDIR. If this is not set, then on Unix systems, vips will default to `/tmp`. On Windows, vips uses `GetTempPath()` to find the temporary directory.

  ```elixir
  vips_image = Image.new_temp_file("%s.v")
  ```
  """
  @spec new_temp_file(String.t()) :: {:ok, __MODULE__.t()} | {:error, term()}
  def new_temp_file(format) do
    Nif.nif_image_new_temp_file(normalize_string(format))
    |> wrap_type()
  end

  @doc """
  Make a VipsImage from 2D list.

  This is a convenience function makes an image which is a matrix: a one-band VIPS_FORMAT_DOUBLE image held in memory. Useful for vips operations such as `conv`.

  ```elixir
  {:ok, mask} = Image.new_matrix_from_array(3, 3, [[0, 1, 0], [1, 1, 1], [0, 1, 0]])
  ```

  ## Optional
  * scale - Default: 1
  * offset - Default: 0
  """
  @spec new_matrix_from_array(integer, integer, list(list), keyword()) ::
          {:ok, __MODULE__.t()} | {:error, term()}
  def new_matrix_from_array(width, height, list, optional \\ []) do
    scale = to_double(optional[:scale], 1)
    offset = to_double(optional[:offset], 0)
    list = flatten_to_double_list(list)

    Nif.nif_image_new_matrix_from_array(width, height, list, scale, offset)
    |> wrap_type()
  end

  @doc """
  Make a VipsImage from 1D or 2D list.

  If list is a single dimension then an image of height 1 will be with list
  content as values.

  If list is 2D then 2D image will be created.

  Output image will always be a one band image with `double` format.

  ```elixir
  # 1D list
  {:ok, img2} = Image.new_from_list([0, 1, 0])

  # 2D list
  {:ok, img} = Image.new_from_list([[0, 1, 0], [1, 1, 1], [0, 1, 0]])
  ```

  ## Optional

  * scale - Default: 1
  * offset - Default: 0
  """
  @spec new_from_list(list([number]) | [number] | Range.t(), keyword()) ::
          {:ok, __MODULE__.t()} | {:error, term()}
  def new_from_list(list, optional \\ []) do
    with {:ok, {width, height, list}} <- validate_matrix_list(list) do
      scale = to_double(optional[:scale], 1)
      offset = to_double(optional[:offset], 0)

      Nif.nif_image_new_matrix_from_array(width, height, list, scale, offset)
      |> wrap_type()
    end
  end

  @doc """
  Mutate an image in-place. You have to pass a function which takes MutableImage as argument. Inside the callback function, you can call functions which modify the image, such as setting or removing metadata. See `Vix.Vips.MutableImage`

  Return value of the callback is ignored.

  Call returns updated image.

  Example

  ```elixir
    {:ok, im} = Image.new_from_file("puppies.jpg")

    {:ok, new_im} =
      Image.mutate(im, fn mut_image ->
        :ok = MutableImage.update(mut_image, "orientation", 0)
        :ok = MutableImage.set(mut_image, "new-field", :gint, 0)
      end)
  ```
  """
  @spec mutate(__MODULE__.t(), (Vix.Vips.MutableImage.t() -> any())) ::
          {:ok, __MODULE__.t()} | {:ok, {__MODULE__.t(), any()}} | {:error, term()}
  def mutate(%Image{} = image, callback) do
    {:ok, mut_image} = MutableImage.new(image)

    try do
      case callback.(mut_image) do
        :ok ->
          MutableImage.to_image(mut_image)

        {:ok, result} ->
          {:ok, image} = MutableImage.to_image(mut_image)
          {:ok, {image, result}}
      end
    after
      MutableImage.stop(mut_image)
    end
  end

  @doc """
  Return a boolean indicating if an image has an alpha band.

  Example

  ```elixir
    {:ok, im} = Image.new_from_file("puppies.jpg")

    has_alpha? = Image.has_alpha?(im)
  ```
  """
  @spec has_alpha?(__MODULE__.t()) :: boolean | no_return()
  def has_alpha?(%Image{ref: vips_image}) do
    case Nif.nif_image_hasalpha(vips_image) do
      {:ok, value} ->
        value

      {:error, reason} ->
        raise reason
    end
  end

  @doc """
  Get all image header field names.

  See https://libvips.github.io/libvips/API/current/libvips-header.html#vips-image-get-fields for more details
  """
  @spec header_field_names(__MODULE__.t()) :: {:ok, [String.t()]} | {:error, term()}
  def header_field_names(%Image{ref: vips_image}) do
    Nif.nif_image_get_fields(vips_image)
  end

  @doc """
  Get image header value.

  This is a generic function to get header value.

  Casts the value to appropriate type. Returned value can be integer, float, string, binary, list. Use `Vix.Vips.Image.header_value_as_string/2` to get string representation of any header value.

  ```elixir
  {:ok, width} = Image.header_value(vips_image, "width")
  ```
  """
  @spec header_value(__MODULE__.t(), String.t()) ::
          {:ok, integer() | float() | String.t() | binary() | list() | atom()} | {:error, term()}
  def header_value(%Image{ref: vips_image}, name) do
    value = Nif.nif_image_get_header(vips_image, normalize_string(name))

    case value do
      {:ok, {type, value}} ->
        {:ok, Vix.Type.to_erl_term(type, value)}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Get image header value as string.

  This is generic method to get string representation of a header value. If value is VipsBlob, then it returns base64 encoded data.

  See: https://libvips.github.io/libvips/API/current/libvips-header.html#vips-image-get-as-string
  """
  @spec header_value_as_string(__MODULE__.t(), String.t()) :: {:ok, String.t()} | {:error, term()}
  def header_value_as_string(%Image{ref: vips_image}, name) do
    Nif.nif_image_get_as_string(vips_image, normalize_string(name))
  end

  for {name, spec} <- %{
        "width" => quote(do: pos_integer()),
        "height" => quote(do: pos_integer()),
        "bands" => quote(do: pos_integer()),
        "xres" => quote(do: float()),
        "yres" => quote(do: float()),
        "xoffset" => quote(do: integer()),
        "yoffset" => quote(do: integer()),
        "filename" => quote(do: String.t()),
        "mode" => quote(do: Stringt.t()),
        "scale" => quote(do: float()),
        "offset" => quote(do: float()),
        "page-height" => quote(do: integer()),
        "n-pages" => quote(do: integer()),
        "orientation" => quote(do: integer()),
        "interpretation" => quote(do: Vix.Vips.Operation.vips_interpretation()),
        "coding" => quote(do: Vix.Vips.Operation.vips_coding()),
        "format" => quote(do: Vix.Vips.Operatoin.vips_band_format())
      } do
    func_name = name |> String.replace("-", "_") |> String.to_atom()

    @doc """
    Get "#{name}" of the image

    > #### More details {: .tip}
    >
    > See [libvips docs](https://libvips.github.io/libvips/API/current/libvips-header.html#vips-image-get-#{name}) for more details regarding `#{func_name}` function

    """
    @spec unquote(func_name)(__MODULE__.t()) :: unquote(spec) | no_return()
    def unquote(func_name)(image) do
      case header_value(image, unquote(name)) do
        {:ok, value} -> value
        {:error, error} -> raise to_string(error)
      end
    end
  end

  @doc """
  Get all image header data as map. Headers includes metadata such as image height, width, bands.

  If a header does not exists then value for that header will be set to `nil`.

  See https://libvips.github.io/libvips/API/current/libvips-header.html for more details.
  """
  @spec headers(__MODULE__.t()) :: %{
          width: pos_integer() | nil,
          height: pos_integer() | nil,
          bands: pos_integer() | nil,
          xres: float() | nil,
          yres: float() | nil,
          xoffset: integer() | nil,
          yoffset: integer() | nil,
          filename: String.t() | nil,
          mode: Stringt.t() | nil,
          scale: float() | nil,
          offset: float() | nil,
          "page-height": integer() | nil,
          "n-pages": integer() | nil,
          orientation: integer() | nil,
          interpretation: Vix.Vips.Operation.vips_interpretation() | nil,
          coding: Vix.Vips.Operation.vips_coding() | nil,
          format: Vix.Vips.Operatoin.vips_band_format() | nil
        }
  def headers(image) do
    [
      :width,
      :height,
      :bands,
      :xres,
      :yres,
      :xoffset,
      :yoffset,
      :filename,
      :mode,
      :scale,
      :offset,
      :"page-height",
      :"n-pages",
      :orientation,
      :interpretation,
      :coding,
      :format
    ]
    |> Map.new(fn field ->
      case header_value(image, to_string(field)) do
        {:ok, value} -> {field, value}
        {:error, _} -> {field, nil}
      end
    end)
  end

  defp normalize_string(str) when is_binary(str), do: str

  defp normalize_string(str) when is_list(str), do: to_string(str)

  defp normalize_list(%Range{} = range), do: Enum.to_list(range)

  defp normalize_list(list) when is_list(list) do
    Enum.map(list, &normalize_list/1)
  end

  defp normalize_list(term), do: term

  defp to_double(v) when is_integer(v), do: v * 1.0
  defp to_double(v) when is_float(v), do: v

  defp to_double(nil, default), do: to_double(default)
  defp to_double(v, _default), do: to_double(v)

  defp wrap_type({:ok, ref}), do: {:ok, %Image{ref: ref}}
  defp wrap_type(value), do: value

  defp normalize_access_args(args) do
    cond do
      Keyword.keyword?(args) ->
        {:ok, Keyword.take(args, ~w(width height band)a)}

      length(args) <= 3 && Enum.all?(args, &(is_integer(&1) || match?(%Range{}, &1))) ->
        {:ok, Enum.zip(~w(width height band)a, args)}

      true ->
        {:error, :invalid_list}
    end
  end

  @spec validate_matrix_list([[number]] | [number] | Range.t()) ::
          {width :: non_neg_integer(), height :: non_neg_integer(), [number]}
  defp validate_matrix_list(list) do
    list = normalize_list(list)

    result =
      cond do
        !is_list(list) ->
          {:error, "argument is not a list"}

        length(list) > 0 && is_list(hd(list)) ->
          height = length(list)
          width = length(hd(list))

          cond do
            !Enum.all?(list, &is_list/1) ->
              {:error, "not a 2D list"}

            !Enum.all?(list, &(length(&1) == width)) ->
              {:error, "list is not rectangular"}

            true ->
              {:ok, {width, height, flatten_to_double_list(list)}}
          end

        true ->
          {:ok, {length(list), 1, cast_to_double_list(list)}}
      end

    with {:ok, {width, height, list}} <- result,
         :ok <- validate_list_dimension(width, height, list),
         :ok <- validate_list_contents(list) do
      {:ok, {width, height, list}}
    end
  end

  defp validate_list_dimension(width, height, list) do
    if length(list) == width * height do
      :ok
    else
      {:error, "bad list dimensions"}
    end
  end

  defp validate_list_contents(list) do
    if Enum.all?(list, &is_number/1) do
      :ok
    else
      {:error, "not all list elements are number"}
    end
  end

  defp cast_to_double_list(list) do
    Enum.map(list, &to_double/1)
  end

  defp flatten_to_double_list(list) do
    Enum.flat_map(list, &cast_to_double_list(&1))
  end

  # Support for rendering images in Livebook

  if Code.ensure_loaded?(Kino.Render) do
    defimpl Kino.Render do
      alias Vix.Vips.Image

      def to_livebook(image) do
        attributes = attributes_from_image(image)
        {:ok, encoded} = Image.write_to_buffer(image, ".png")
        image = Kino.Image.new(encoded, :png)
        tabs = Kino.Layout.tabs(Image: image, Attributes: attributes)
        Kino.Render.to_livebook(tabs)
      end

      defp attributes_from_image(image) do
        data =
          for field <- ~w(width height bands interpretation format filename) do
            case Image.header_value(image, field) do
              {:ok, value} ->
                {String.capitalize(field), value}

              {:error, _error} ->
                nil
            end
          end
          |> Enum.filter(& &1)

        data =
          (data ++ [{"Has alpha band?", Image.has_alpha?(image)}])
          |> Enum.map(fn {k, v} -> [{"Attribute", k}, {"Value", v}] end)

        Kino.DataTable.new(data, name: "Image Metadata")
      end
    end
  end
end