"""A wrapper for Kernels."""
from __future__ import annotations

from typing import Optional

from ee import _arg_types
from ee import apifunction
from ee import computedobject


class Kernel(computedobject.ComputedObject):
  """An object to represent an Earth Engine Kernel.

  Example:
    # Square kernel
    ee.Kernel.square(radius=2, units='pixels', normalize=False)
    # Chebyshev kernel
    ee.Kernel.chebyshev(radius=3)
  """

  _initialized: bool = False

  def __init__(self, kernel: computedobject.ComputedObject):
    """Creates a Kernel wrapper.

    Args:
      kernel: A Kernel to cast.
    """
    self.initialize()

    if isinstance(kernel, computedobject.ComputedObject):
      # There is no server-side constructor for ee.Kernel. Pass the object as-is
      # to the server in case it is intended to be a Kernel cast.
      super().__init__(kernel.func, kernel.args, kernel.varName)
      return

    raise TypeError(
        f'Kernel can only be used as a cast to Kernel. Found {type(kernel)}.'
    )

  @classmethod
  def initialize(cls) -> None:
    """Imports API functions to this class."""
    if not cls._initialized:
      apifunction.ApiFunction.importApi(cls, cls.name(), cls.name())
      cls._initialized = True

  @classmethod
  def reset(cls) -> None:
    """Removes imported API functions from this class."""
    apifunction.ApiFunction.clearApi(cls)
    cls._initialized = False

  @staticmethod
  def name() -> str:
    return 'Kernel'

  def add(
      self,
      kernel2: _arg_types.Kernel,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns the addition of two kernels.

    Does pointwise addition after aligning their centers.

    Args:
      kernel2: The second kernel.
      normalize: Normalize the kernel.

    Returns:
      An ee.Kernel.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.add', self, kernel2, normalize
    )

  @staticmethod
  def chebyshev(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a Chebyshev kernel.

    Generates a distance kernel based on Chebyshev distance (greatest distance
    along any dimension).

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.chebyshev', radius, units, normalize, magnitude
    )

  @staticmethod
  def circle(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a circle-shaped boolean kernel.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.circle', radius, units, normalize, magnitude
    )

  @staticmethod
  def compass(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 3x3 Prewitt's Compass edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_('Kernel.compass', magnitude, normalize)

  @staticmethod
  def cross(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a cross-shaped boolean kernel.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.cross', radius, units, normalize, magnitude
    )

  @staticmethod
  def diamond(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a diamond-shaped boolean kernel.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.diamond', radius, units, normalize, magnitude
    )

  @staticmethod
  def euclidean(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a distance kernel based on Euclidean (straight-line) distance.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.euclidean', radius, units, normalize, magnitude
    )

  # TODO: Test missing weights.
  @staticmethod
  def fixed(
      width: Optional[_arg_types.Integer] = None,
      height: Optional[_arg_types.Integer] = None,
      weights: Optional[_arg_types.List] = None,
      x: Optional[_arg_types.Integer] = None,
      y: Optional[_arg_types.Integer] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a kernel with the given weights.

    Args:
      width: The width of the kernel in pixels.
      height: The height of the kernel in pixels.
      weights: A 2-D list of [height] x [width] values to use as the weights of
        the kernel.
      x: The location of the focus, as an offset from the left.
      y: The location of the focus, as an offset from the top.
      normalize: Normalize the kernel values to sum to 1.
    """
    if weights is None:
      raise ValueError('weights is required.')

    return apifunction.ApiFunction.call_(
        'Kernel.fixed', width, height, weights, x, y, normalize
    )

  @staticmethod
  def gaussian(
      radius: _arg_types.Number,
      sigma: Optional[_arg_types.Number] = None,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a Gaussian kernel from a sampled continuous Gaussian.

    Args:
      radius: The radius of the kernel to generate.
      sigma: Standard deviation of the Gaussian function (same units as radius).
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.gaussian', radius, sigma, units, normalize, magnitude
    )

  def inverse(self) -> Kernel:
    """Returns a kernel which has each of its weights multiplicatively inverted.

    Weights with a value of zero are not inverted and remain zero.
    """

    return apifunction.ApiFunction.call_(self.name() + '.inverse', self)

  @staticmethod
  def kirsch(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 3x3 Kirsch's Compass edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_('Kernel.kirsch', magnitude, normalize)

  @staticmethod
  def laplacian4(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 3x3 Laplacian-4 edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.laplacian4', magnitude, normalize
    )

  @staticmethod
  def laplacian8(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 3x3 Laplacian-8 edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.laplacian8', magnitude, normalize
    )

  @staticmethod
  def manhattan(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a distance kernel based on rectilinear (city-block) distance.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.manhattan', radius, units, normalize, magnitude
    )

  @staticmethod
  def octagon(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns an octagon-shaped boolean kernel.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.octagon', radius, units, normalize, magnitude
    )

  @staticmethod
  def plus(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a plus-shaped boolean kernel.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.plus', radius, units, normalize, magnitude
    )

  @staticmethod
  def prewitt(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 3x3 Prewitt edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_('Kernel.prewitt', magnitude, normalize)

  @staticmethod
  def rectangle(
      xRadius: _arg_types.Number,  # pylint: disable=invalid-name
      yRadius: _arg_types.Number,  # pylint: disable=invalid-name
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a rectangular-shaped kernel.

    Args:
      xRadius: The horizontal radius of the kernel to generate.
      yRadius: The vertical radius of the kernel to generate.
      units: The system of measurement for the kernel ("pixels" or "meters"). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.rectangle', xRadius, yRadius, units, normalize, magnitude
    )

  @staticmethod
  def roberts(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 2x2 Roberts edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_('Kernel.roberts', magnitude, normalize)

  def rotate(self, rotations: _arg_types.Integer) -> Kernel:
    """Returns a rotated kernel.

    Args:
      rotations: Number of 90 degree rotations to make. Negative numbers rotate
        counterclockwise.
    """

    return apifunction.ApiFunction.call_(
        self.name() + '.rotate', self, rotations
    )

  @staticmethod
  def sobel(
      magnitude: Optional[_arg_types.Number] = None,
      normalize: Optional[_arg_types.Bool] = None,
  ) -> Kernel:
    """Returns a 3x3 Sobel edge-detection kernel.

    Args:
      magnitude: Scale each value by this amount.
      normalize: Normalize the kernel values to sum to 1.
    """

    return apifunction.ApiFunction.call_('Kernel.sobel', magnitude, normalize)

  @staticmethod
  def square(
      radius: _arg_types.Number,
      units: Optional[_arg_types.String] = None,
      normalize: Optional[_arg_types.Bool] = None,
      magnitude: Optional[_arg_types.Number] = None,
  ) -> Kernel:
    """Returns a square-shaped boolean kernel.

    Args:
      radius: The radius of the kernel to generate.
      units: The system of measurement for the kernel ('pixels' or 'meters'). If
        the kernel is specified in meters, it will resize when the zoom-level is
        changed.
      normalize: Normalize the kernel values to sum to 1.
      magnitude: Scale each value by this amount.
    """

    return apifunction.ApiFunction.call_(
        'Kernel.square', radius, units, normalize, magnitude
    )
