from __future__ import annotations

from collections.abc import Iterable, Sequence
from typing import TYPE_CHECKING, Any, Literal

from public import public

import ibis
import ibis.common.exceptions as com
import ibis.expr.builders as bl
import ibis.expr.datatypes as dt
import ibis.expr.operations as ops
from ibis.common.deferred import Deferred, _, deferrable
from ibis.common.grounds import Singleton
from ibis.expr.rewrites import rewrite_window_input
from ibis.expr.types.core import Expr, _binop, _FixedTextJupyterMixin
from ibis.util import deprecated

if TYPE_CHECKING:
    import pandas as pd
    import pyarrow as pa

    import ibis.expr.types as ir
    from ibis.formats.pyarrow import PyArrowData


@public
class Value(Expr):
    """Base class for a data generating expression having a known type."""

    def name(self, name):
        """Rename an expression to `name`.

        Parameters
        ----------
        name
            The new name of the expression

        Returns
        -------
        Value
            `self` with name `name`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"a": [1, 2]}, name="t")
        >>> t.a
        ┏━━━━━━━┓
        ┃ a     ┃
        ┡━━━━━━━┩
        │ int64 │
        ├───────┤
        │     1 │
        │     2 │
        └───────┘
        >>> t.a.name("b")
        ┏━━━━━━━┓
        ┃ b     ┃
        ┡━━━━━━━┩
        │ int64 │
        ├───────┤
        │     1 │
        │     2 │
        └───────┘
        """
        # TODO(kszucs): shouldn't do simplification here, but rather later
        # when simplifying the whole operation tree
        # the expression's name is idendical to the new one
        if self.has_name() and self.get_name() == name:
            return self

        if isinstance(self.op(), ops.Alias):
            # only keep a single alias operation
            op = ops.Alias(arg=self.op().arg, name=name)
        else:
            op = ops.Alias(arg=self, name=name)

        return op.to_expr()

    # TODO(kszucs): should rename to dtype
    def type(self) -> dt.DataType:
        """Return the [DataType](./datatypes.qmd) of `self`."""
        return self.op().dtype

    def hash(self) -> ir.IntegerValue:
        """Compute an integer hash value.

        ::: {.callout-note}
        ## The hashing function used is backend-dependent.
        :::

        Returns
        -------
        IntegerValue
            The hash value of `self`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> ibis.literal("hello").hash()  # doctest: +SKIP
        -4155090522938856779
        """
        return ops.Hash(self).to_expr()

    def cast(self, target_type: Any) -> Value:
        """Cast expression to indicated data type.

        Similar to `pandas.Series.astype`.

        Parameters
        ----------
        target_type
            Type to cast to. Anything accepted by [`ibis.dtype()`](./datatypes.qmd#ibis.dtype)

        Returns
        -------
        Value
            Casted expression

        See Also
        --------
        [`Value.try_cast()`](./expression-generic.qmd#ibis.expr.types.generic.Value.try_cast)
        [`ibis.dtype()`](./datatypes.qmd#ibis.dtype)

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> x = ibis.examples.penguins.fetch()["bill_depth_mm"]
        >>> x
        ┏━━━━━━━━━━━━━━━┓
        ┃ bill_depth_mm ┃
        ┡━━━━━━━━━━━━━━━┩
        │ float64       │
        ├───────────────┤
        │          18.7 │
        │          17.4 │
        │          18.0 │
        │          NULL │
        │          19.3 │
        │          20.6 │
        │          17.8 │
        │          19.6 │
        │          18.1 │
        │          20.2 │
        │             … │
        └───────────────┘

        python's built-in types can be used

        >>> x.cast(int)
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ Cast(bill_depth_mm, int64) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ int64                      │
        ├────────────────────────────┤
        │                         19 │
        │                         17 │
        │                         18 │
        │                       NULL │
        │                         19 │
        │                         21 │
        │                         18 │
        │                         20 │
        │                         18 │
        │                         20 │
        │                          … │
        └────────────────────────────┘

        or string names

        >>> x.cast("uint16")
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ Cast(bill_depth_mm, uint16) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ uint16                      │
        ├─────────────────────────────┤
        │                          19 │
        │                          17 │
        │                          18 │
        │                        NULL │
        │                          19 │
        │                          21 │
        │                          18 │
        │                          20 │
        │                          18 │
        │                          20 │
        │                           … │
        └─────────────────────────────┘

        If you make an illegal cast, you won't know until the backend actually
        executes it. Consider [`.try_cast()`](#ibis.expr.types.generic.Value.try_cast).

        >>> ibis.literal("a string").cast("int64")  # doctest: +SKIP
        <error>
        """
        op = ops.Cast(self, to=target_type)

        if op.to == self.type():
            # noop case if passed type is the same
            return self

        if op.to.is_geospatial():
            from_geotype = self.type().geotype or "geometry"
            to_geotype = op.to.geotype
            if from_geotype == to_geotype:
                return self

        return op.to_expr()

    def try_cast(self, target_type: Any) -> Value:
        """Try cast expression to indicated data type.

        If the cast fails for a row, the value is returned
        as null or NaN depending on target_type and backend behavior.

        Parameters
        ----------
        target_type
            Type to try cast to. Anything accepted by [`ibis.dtype()`](./datatypes.qmd#ibis.dtype)

        Returns
        -------
        Value
            Casted expression

        See Also
        --------
        [`Value.cast()`](./expression-generic.qmd#ibis.expr.types.generic.Value.cast)
        [`ibis.dtype()`](./datatypes.qmd#ibis.dtype)

        Examples
        --------
        >>> import ibis
        >>> from ibis import _
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"numbers": [1, 2, 3, 4], "strings": ["1.0", "2", "hello", "world"]})
        >>> t
        ┏━━━━━━━━━┳━━━━━━━━━┓
        ┃ numbers ┃ strings ┃
        ┡━━━━━━━━━╇━━━━━━━━━┩
        │ int64   │ string  │
        ├─────────┼─────────┤
        │       1 │ 1.0     │
        │       2 │ 2       │
        │       3 │ hello   │
        │       4 │ world   │
        └─────────┴─────────┘
        >>> t = t.mutate(numbers_to_strings=_.numbers.try_cast("string"))
        >>> t = t.mutate(strings_to_numbers=_.strings.try_cast("int"))
        >>> t
        ┏━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━┓
        ┃ numbers ┃ strings ┃ numbers_to_strings ┃ strings_to_numbers ┃
        ┡━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━┩
        │ int64   │ string  │ string             │ int64              │
        ├─────────┼─────────┼────────────────────┼────────────────────┤
        │       1 │ 1.0     │ 1                  │                  1 │
        │       2 │ 2       │ 2                  │                  2 │
        │       3 │ hello   │ 3                  │               NULL │
        │       4 │ world   │ 4                  │               NULL │
        └─────────┴─────────┴────────────────────┴────────────────────┘
        """
        op = ops.TryCast(self, to=target_type)

        if op.to == self.type():
            # noop case if passed type is the same
            return self

        return op.to_expr()

    def coalesce(self, *args: Value) -> Value:
        """Return the first non-null value from `args`.

        Parameters
        ----------
        args
            Arguments from which to choose the first non-null value

        Returns
        -------
        Value
            Coalesced expression

        See Also
        --------
        [`ibis.coalesce()`](./expression-generic.qmd#ibis.coalesce)
        [`Value.fillna()`](./expression-generic.qmd#ibis.expr.types.generic.Value.fillna)

        Examples
        --------
        >>> import ibis
        >>> ibis.coalesce(None, 4, 5).name("x")
        x: Coalesce(...)
        """
        return ops.Coalesce((self, *args)).to_expr()

    @deprecated(as_of="8.0.0", instead="use ibis.greatest(self, rest...) instead")
    def greatest(self, *args: ir.Value) -> ir.Value:
        return ops.Greatest((self, *args)).to_expr()

    @deprecated(as_of="8.0.0", instead="use ibis.least(self, rest...) instead")
    def least(self, *args: ir.Value) -> ir.Value:
        return ops.Least((self, *args)).to_expr()

    def typeof(self) -> ir.StringValue:
        """Return the string name of the datatype of self.

        The values of the returned strings are necessarily backend dependent.
        e.g. duckdb may say "DOUBLE", while sqlite may say "real".

        Returns
        -------
        StringValue
            A string indicating the type of the value

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> vals = ibis.examples.penguins.fetch().head(5).bill_length_mm
        >>> vals
        ┏━━━━━━━━━━━━━━━━┓
        ┃ bill_length_mm ┃
        ┡━━━━━━━━━━━━━━━━┩
        │ float64        │
        ├────────────────┤
        │           39.1 │
        │           39.5 │
        │           40.3 │
        │           NULL │
        │           36.7 │
        └────────────────┘
        >>> vals.typeof()
        ┏━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ TypeOf(bill_length_mm) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ string                 │
        ├────────────────────────┤
        │ DOUBLE                 │
        │ DOUBLE                 │
        │ DOUBLE                 │
        │ DOUBLE                 │
        │ DOUBLE                 │
        └────────────────────────┘

        Different backends have different names for their native types

        >>> ibis.duckdb.connect().execute(ibis.literal(5.4).typeof())
        'DOUBLE'
        >>> ibis.sqlite.connect().execute(ibis.literal(5.4).typeof())
        'real'
        """
        return ops.TypeOf(self).to_expr()

    def fillna(self, fill_value: Scalar) -> Value:
        """Replace any null values with the indicated fill value.

        Parameters
        ----------
        fill_value
            Value with which to replace `NA` values in `self`

        See Also
        --------
        [`Value.coalesce()`](./expression-generic.qmd#ibis.expr.types.generic.Value.coalesce)
        [`ibis.coalesce()`](./expression-generic.qmd#ibis.coalesce)

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t.sex
        ┏━━━━━━━━┓
        ┃ sex    ┃
        ┡━━━━━━━━┩
        │ string │
        ├────────┤
        │ male   │
        │ female │
        │ female │
        │ NULL   │
        │ female │
        └────────┘
        >>> t.sex.fillna("unrecorded").name("sex")
        ┏━━━━━━━━━━━━┓
        ┃ sex        ┃
        ┡━━━━━━━━━━━━┩
        │ string     │
        ├────────────┤
        │ male       │
        │ female     │
        │ female     │
        │ unrecorded │
        │ female     │
        └────────────┘

        Returns
        -------
        Value
            `self` filled with `fill_value` where it is `NA`
        """
        return ops.Coalesce((self, fill_value)).to_expr()

    def nullif(self, null_if_expr: Value) -> Value:
        """Set values to null if they equal the values `null_if_expr`.

        Commonly used to avoid divide-by-zero problems by replacing zero with
        `NULL` in the divisor.

        Equivalent to `(self == null_if_expr).ifelse(ibis.null(), self)`.

        Parameters
        ----------
        null_if_expr
            Expression indicating what values should be NULL

        Returns
        -------
        Value
            Value expression

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> vals = ibis.examples.penguins.fetch().head(5).sex
        >>> vals
        ┏━━━━━━━━┓
        ┃ sex    ┃
        ┡━━━━━━━━┩
        │ string │
        ├────────┤
        │ male   │
        │ female │
        │ female │
        │ NULL   │
        │ female │
        └────────┘
        >>> vals.nullif("male")
        ┏━━━━━━━━━━━━━━━━━━━━━┓
        ┃ NullIf(sex, 'male') ┃
        ┡━━━━━━━━━━━━━━━━━━━━━┩
        │ string              │
        ├─────────────────────┤
        │ NULL                │
        │ female              │
        │ female              │
        │ NULL                │
        │ female              │
        └─────────────────────┘
        """
        return ops.NullIf(self, null_if_expr).to_expr()

    def between(
        self,
        lower: Value,
        upper: Value,
    ) -> ir.BooleanValue:
        """Check if this expression is between `lower` and `upper`, inclusive.

        Parameters
        ----------
        lower
            Lower bound, inclusive
        upper
            Upper bound, inclusive

        Returns
        -------
        BooleanValue
            Expression indicating membership in the provided range

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t.bill_length_mm.between(35, 38)
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ Between(bill_length_mm, 35, 38) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ boolean                         │
        ├─────────────────────────────────┤
        │ False                           │
        │ False                           │
        │ False                           │
        │ NULL                            │
        │ True                            │
        └─────────────────────────────────┘
        """
        return ops.Between(self, lower, upper).to_expr()

    def isin(self, values: Value | Sequence[Value]) -> ir.BooleanValue:
        """Check whether this expression's values are in `values`.

        Parameters
        ----------
        values
            Values or expression to check for membership

        Returns
        -------
        BooleanValue
            Expression indicating membership

        See Also
        --------
        [`Value.notin()`](./expression-generic.qmd#ibis.expr.types.generic.Value.notin)

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"a": [1, 2, 3], "b": [2, 3, 4]})
        >>> t
        ┏━━━━━━━┳━━━━━━━┓
        ┃ a     ┃ b     ┃
        ┡━━━━━━━╇━━━━━━━┩
        │ int64 │ int64 │
        ├───────┼───────┤
        │     1 │     2 │
        │     2 │     3 │
        │     3 │     4 │
        └───────┴───────┘

        Check against a literal sequence of values

        >>> t.a.isin([1, 2])
        ┏━━━━━━━━━━━━━┓
        ┃ InValues(a) ┃
        ┡━━━━━━━━━━━━━┩
        │ boolean     │
        ├─────────────┤
        │ True        │
        │ True        │
        │ False       │
        └─────────────┘

        Check against a derived expression

        >>> t.a.isin(t.b + 1)
        ┏━━━━━━━━━━━━━━━┓
        ┃ InSubquery(a) ┃
        ┡━━━━━━━━━━━━━━━┩
        │ boolean       │
        ├───────────────┤
        │ False         │
        │ False         │
        │ True          │
        └───────────────┘

        Check against a column from a different table

        >>> t2 = ibis.memtable({"x": [99, 2, 99]})
        >>> t.a.isin(t2.x)
        ┏━━━━━━━━━━━━━━━┓
        ┃ InSubquery(a) ┃
        ┡━━━━━━━━━━━━━━━┩
        │ boolean       │
        ├───────────────┤
        │ False         │
        │ True          │
        │ False         │
        └───────────────┘
        """
        from ibis.expr.types import ArrayValue

        if isinstance(values, ArrayValue):
            return ops.ArrayContains(values, self).to_expr()
        elif isinstance(values, Column):
            return ops.InSubquery(values.as_table(), needle=self).to_expr()
        else:
            return ops.InValues(self, values).to_expr()

    def notin(self, values: Value | Sequence[Value]) -> ir.BooleanValue:
        """Check whether this expression's values are not in `values`.

        Opposite of [`Value.isin()`](./expression-generic.qmd#ibis.expr.types.generic.Value.isin).

        Parameters
        ----------
        values
            Values or expression to check for lack of membership

        Returns
        -------
        BooleanValue
            Whether `self`'s values are not contained in `values`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t.bill_depth_mm
        ┏━━━━━━━━━━━━━━━┓
        ┃ bill_depth_mm ┃
        ┡━━━━━━━━━━━━━━━┩
        │ float64       │
        ├───────────────┤
        │          18.7 │
        │          17.4 │
        │          18.0 │
        │          NULL │
        │          19.3 │
        └───────────────┘
        >>> t.bill_depth_mm.notin([18.7, 18.1])
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ Not(InValues(bill_depth_mm)) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ boolean                      │
        ├──────────────────────────────┤
        │ False                        │
        │ True                         │
        │ True                         │
        │ NULL                         │
        │ True                         │
        └──────────────────────────────┘
        """
        return ~self.isin(values)

    def substitute(
        self,
        value: Value | dict,
        replacement: Value | None = None,
        else_: Value | None = None,
    ):
        """Replace values given in `values` with `replacement`.

        This is similar to the pandas `replace` method.

        Parameters
        ----------
        value
            Expression or dict.
        replacement
            If an expression is passed to value, this must be
            passed.
        else_
            If an original value does not match `value`, then `else_` is used.
            The default of `None` means leave the original value unchanged.

        Returns
        -------
        Value
            Replaced values

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.island.value_counts().order_by("island")
        ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
        ┃ island    ┃ island_count ┃
        ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━┩
        │ string    │ int64        │
        ├───────────┼──────────────┤
        │ Biscoe    │          168 │
        │ Dream     │          124 │
        │ Torgersen │           52 │
        └───────────┴──────────────┘
        >>> t.island.substitute({"Torgersen": "torg", "Biscoe": "bisc"}).name(
        ...     "island"
        ... ).value_counts().order_by("island")
        ┏━━━━━━━━┳━━━━━━━━━━━━━━┓
        ┃ island ┃ island_count ┃
        ┡━━━━━━━━╇━━━━━━━━━━━━━━┩
        │ string │ int64        │
        ├────────┼──────────────┤
        │ Dream  │          124 │
        │ bisc   │          168 │
        │ torg   │           52 │
        └────────┴──────────────┘
        """
        if isinstance(value, dict):
            expr = ibis.case()
            try:
                null_replacement = value.pop(None)
            except KeyError:
                pass
            else:
                expr = expr.when(self.isnull(), null_replacement)
            for k, v in value.items():
                expr = expr.when(self == k, v)
        else:
            expr = self.case().when(value, replacement)

        return expr.else_(else_ if else_ is not None else self).end()

    def over(
        self,
        window=None,
        *,
        rows=None,
        range=None,
        group_by=None,
        order_by=None,
    ) -> Value:
        """Construct a window expression.

        Parameters
        ----------
        window
            Window specification
        rows
            Whether to use the `ROWS` window clause
        range
            Whether to use the `RANGE` window clause
        group_by
            Grouping key
        order_by
            Ordering key

        Returns
        -------
        Value
            A window function expression

        """
        node = self.op()
        if window is None:
            window = ibis.window(
                rows=rows,
                range=range,
                group_by=group_by,
                order_by=order_by,
            )
        elif not isinstance(window, bl.WindowBuilder):
            raise com.IbisTypeError("Unexpected window type: {window!r}")

        if len(node.relations) == 0:
            table = None
        elif len(node.relations) == 1:
            (table,) = node.relations
        else:
            raise com.RelationError("Cannot use window with multiple tables")

        @deferrable
        def bind(table):
            frame = window.bind(table)
            winfunc = rewrite_window_input(node, frame)
            if winfunc == node:
                raise com.IbisTypeError(
                    "No reduction or analytic function found to construct a window expression"
                )
            return winfunc.to_expr()

        try:
            return bind(table)
        except com.IbisInputError:
            return bind(_)

    def isnull(self) -> ir.BooleanValue:
        """Return whether this expression is NULL.

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t.bill_depth_mm
        ┏━━━━━━━━━━━━━━━┓
        ┃ bill_depth_mm ┃
        ┡━━━━━━━━━━━━━━━┩
        │ float64       │
        ├───────────────┤
        │          18.7 │
        │          17.4 │
        │          18.0 │
        │          NULL │
        │          19.3 │
        └───────────────┘
        >>> t.bill_depth_mm.isnull()
        ┏━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ IsNull(bill_depth_mm) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━┩
        │ boolean               │
        ├───────────────────────┤
        │ False                 │
        │ False                 │
        │ False                 │
        │ True                  │
        │ False                 │
        └───────────────────────┘
        """
        return ops.IsNull(self).to_expr()

    def notnull(self) -> ir.BooleanValue:
        """Return whether this expression is not NULL.

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t.bill_depth_mm
        ┏━━━━━━━━━━━━━━━┓
        ┃ bill_depth_mm ┃
        ┡━━━━━━━━━━━━━━━┩
        │ float64       │
        ├───────────────┤
        │          18.7 │
        │          17.4 │
        │          18.0 │
        │          NULL │
        │          19.3 │
        └───────────────┘
        >>> t.bill_depth_mm.notnull()
        ┏━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ NotNull(bill_depth_mm) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ boolean                │
        ├────────────────────────┤
        │ True                   │
        │ True                   │
        │ True                   │
        │ False                  │
        │ True                   │
        └────────────────────────┘
        """
        return ops.NotNull(self).to_expr()

    def case(self) -> bl.SimpleCaseBuilder:
        """Create a SimpleCaseBuilder to chain multiple if-else statements.

        Add new search expressions with the `.when()` method. These must be
        comparable with this column expression. Conclude by calling `.end()`.

        Returns
        -------
        SimpleCaseBuilder
            A case builder

        See Also
        --------
        [`Value.substitute()`](./expression-generic.qmd#ibis.expr.types.generic.Value.substitute)
        [`ibis.cases()`](./expression-generic.qmd#ibis.expr.types.generic.Value.cases)
        [`ibis.case()`](./expression-generic.qmd#ibis.case)

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> x = ibis.examples.penguins.fetch().head(5)["sex"]
        >>> x
        ┏━━━━━━━━┓
        ┃ sex    ┃
        ┡━━━━━━━━┩
        │ string │
        ├────────┤
        │ male   │
        │ female │
        │ female │
        │ NULL   │
        │ female │
        └────────┘
        >>> x.case().when("male", "M").when("female", "F").else_("U").end()
        ┏━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ SimpleCase(sex, 'U') ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━┩
        │ string               │
        ├──────────────────────┤
        │ M                    │
        │ F                    │
        │ F                    │
        │ U                    │
        │ F                    │
        └──────────────────────┘

        Cases not given result in the ELSE case

        >>> x.case().when("male", "M").else_("OTHER").end()
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ SimpleCase(sex, 'OTHER') ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ string                   │
        ├──────────────────────────┤
        │ M                        │
        │ OTHER                    │
        │ OTHER                    │
        │ OTHER                    │
        │ OTHER                    │
        └──────────────────────────┘

        If you don't supply an ELSE, then NULL is used

        >>> x.case().when("male", "M").end()
        ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ SimpleCase(sex, Cast(None, string)) ┃
        ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
        │ string                              │
        ├─────────────────────────────────────┤
        │ M                                   │
        │ NULL                                │
        │ NULL                                │
        │ NULL                                │
        │ NULL                                │
        └─────────────────────────────────────┘
        """
        import ibis.expr.builders as bl

        return bl.SimpleCaseBuilder(self.op())

    def cases(
        self,
        case_result_pairs: Iterable[tuple[ir.BooleanValue, Value]],
        default: Value | None = None,
    ) -> Value:
        """Create a case expression in one shot.

        Parameters
        ----------
        case_result_pairs
            Conditional-result pairs
        default
            Value to return if none of the case conditions are true

        Returns
        -------
        Value
            Value expression

        See Also
        --------
        [`Value.substitute()`](./expression-generic.qmd#ibis.expr.types.generic.Value.substitute)
        [`ibis.cases()`](./expression-generic.qmd#ibis.expr.types.generic.Value.cases)
        [`ibis.case()`](./expression-generic.qmd#ibis.case)

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"values": [1, 2, 1, 2, 3, 2, 4]})
        >>> t
        ┏━━━━━━━━┓
        ┃ values ┃
        ┡━━━━━━━━┩
        │ int64  │
        ├────────┤
        │      1 │
        │      2 │
        │      1 │
        │      2 │
        │      3 │
        │      2 │
        │      4 │
        └────────┘
        >>> number_letter_map = ((1, "a"), (2, "b"), (3, "c"))
        >>> t.values.cases(number_letter_map, default="unk").name("replace")
        ┏━━━━━━━━━┓
        ┃ replace ┃
        ┡━━━━━━━━━┩
        │ string  │
        ├─────────┤
        │ a       │
        │ b       │
        │ a       │
        │ b       │
        │ c       │
        │ b       │
        │ unk     │
        └─────────┘
        """
        builder = self.case()
        for case, result in case_result_pairs:
            builder = builder.when(case, result)
        return builder.else_(default).end()

    def collect(self, where: ir.BooleanValue | None = None) -> ir.ArrayScalar:
        """Aggregate this expression's elements into an array.

        This function is called `array_agg`, `list_agg`, or `list` in other systems.

        Parameters
        ----------
        where
            Filter to apply before aggregation

        Returns
        -------
        ArrayScalar
            Collected array

        Examples
        --------
        Basic collect usage

        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"key": list("aaabb"), "value": [1, 2, 3, 4, 5]})
        >>> t
        ┏━━━━━━━━┳━━━━━━━┓
        ┃ key    ┃ value ┃
        ┡━━━━━━━━╇━━━━━━━┩
        │ string │ int64 │
        ├────────┼───────┤
        │ a      │     1 │
        │ a      │     2 │
        │ a      │     3 │
        │ b      │     4 │
        │ b      │     5 │
        └────────┴───────┘
        >>> t.value.collect()
        [1, 2, ... +3]
        >>> type(t.value.collect())
        <class 'ibis.expr.types.arrays.ArrayScalar'>

        Collect elements per group

        >>> t.group_by("key").agg(v=lambda t: t.value.collect()).order_by("key")
        ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ key    ┃ v                    ┃
        ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
        │ string │ array<int64>         │
        ├────────┼──────────────────────┤
        │ a      │ [1, 2, ... +1]       │
        │ b      │ [4, 5]               │
        └────────┴──────────────────────┘

        Collect elements per group using a filter

        >>> t.group_by("key").agg(v=lambda t: t.value.collect(where=t.value > 1)).order_by("key")
        ┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
        ┃ key    ┃ v                    ┃
        ┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
        │ string │ array<int64>         │
        ├────────┼──────────────────────┤
        │ a      │ [2, 3]               │
        │ b      │ [4, 5]               │
        └────────┴──────────────────────┘
        """
        return ops.ArrayCollect(
            self, where=self._bind_reduction_filter(where)
        ).to_expr()

    def identical_to(self, other: Value) -> ir.BooleanValue:
        """Return whether this expression is identical to other.

        Corresponds to `IS NOT DISTINCT FROM` in SQL.

        Parameters
        ----------
        other
            Expression to compare to

        Returns
        -------
        BooleanValue
            Whether this expression is not distinct from `other`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> one = ibis.literal(1)
        >>> two = ibis.literal(2)
        >>> two.identical_to(one + one)
        True
        """
        try:
            return ops.IdenticalTo(self, other).to_expr()
        except (com.IbisTypeError, NotImplementedError):
            return NotImplemented

    def group_concat(
        self,
        sep: str = ",",
        where: ir.BooleanValue | None = None,
    ) -> ir.StringScalar:
        """Concatenate values using the indicated separator to produce a string.

        Parameters
        ----------
        sep
            Separator will be used to join strings
        where
            Filter expression

        Returns
        -------
        StringScalar
            Concatenated string expression

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t[["bill_length_mm", "bill_depth_mm"]]
        ┏━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
        ┃ bill_length_mm ┃ bill_depth_mm ┃
        ┡━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
        │ float64        │ float64       │
        ├────────────────┼───────────────┤
        │           39.1 │          18.7 │
        │           39.5 │          17.4 │
        │           40.3 │          18.0 │
        │           NULL │          NULL │
        │           36.7 │          19.3 │
        └────────────────┴───────────────┘
        >>> t.bill_length_mm.group_concat()
        '39.1,39.5,40.3,36.7'

        >>> t.bill_length_mm.group_concat(sep=": ")
        '39.1: 39.5: 40.3: 36.7'

        >>> t.bill_length_mm.group_concat(sep=": ", where=t.bill_depth_mm > 18)
        '39.1: 36.7'
        """
        return ops.GroupConcat(
            self, sep=sep, where=self._bind_reduction_filter(where)
        ).to_expr()

    def __hash__(self) -> int:
        return super().__hash__()

    def __eq__(self, other: Value) -> ir.BooleanValue:
        if other is None:
            return _binop(ops.IdenticalTo, self, other)
        return _binop(ops.Equals, self, other)

    def __ne__(self, other: Value) -> ir.BooleanValue:
        if other is None:
            return ~self.__eq__(other)
        return _binop(ops.NotEquals, self, other)

    def __ge__(self, other: Value) -> ir.BooleanValue:
        return _binop(ops.GreaterEqual, self, other)

    def __gt__(self, other: Value) -> ir.BooleanValue:
        return _binop(ops.Greater, self, other)

    def __le__(self, other: Value) -> ir.BooleanValue:
        return _binop(ops.LessEqual, self, other)

    def __lt__(self, other: Value) -> ir.BooleanValue:
        return _binop(ops.Less, self, other)

    def asc(self) -> ir.Value:
        """Sort an expression ascending."""
        return ops.SortKey(self, ascending=True).to_expr()

    def desc(self) -> ir.Value:
        """Sort an expression descending."""
        return ops.SortKey(self, ascending=False).to_expr()

    def to_pandas(self, **kwargs) -> pd.Series:
        """Convert a column expression to a pandas Series or scalar object.

        Parameters
        ----------
        kwargs
            Same as keyword arguments to [`execute`](#ibis.expr.types.core.Expr.execute)

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch().limit(5)
        >>> t.to_pandas()
          species     island  bill_length_mm  ...  body_mass_g     sex  year
        0  Adelie  Torgersen            39.1  ...       3750.0    male  2007
        1  Adelie  Torgersen            39.5  ...       3800.0  female  2007
        2  Adelie  Torgersen            40.3  ...       3250.0  female  2007
        3  Adelie  Torgersen             NaN  ...          NaN    None  2007
        4  Adelie  Torgersen            36.7  ...       3450.0  female  2007
        [5 rows x 8 columns]
        """
        return self.execute(**kwargs)


@public
class Scalar(Value):
    def __interactive_rich_console__(self, console, options):
        import rich.pretty

        interactive = ibis.options.repr.interactive
        return console.render(
            rich.pretty.Pretty(
                self.execute(),
                max_length=interactive.max_length,
                max_string=interactive.max_string,
                max_depth=interactive.max_depth,
            ),
            options=options,
        )

    def __pyarrow_result__(
        self, table: pa.Table, data_mapper: type[PyArrowData] | None = None
    ) -> pa.Scalar:
        if data_mapper is None:
            from ibis.formats.pyarrow import PyArrowData as data_mapper

        return data_mapper.convert_scalar(table[0][0], self.type())

    def __pandas_result__(self, df: pd.DataFrame) -> Any:
        from ibis.formats.pandas import PandasData

        return PandasData.convert_scalar(df, self.type())

    def as_scalar(self):
        """Inform ibis that the expression should be treated as a scalar.

        If the expression is a literal, it will be returned as is. If it depends
        on a table, it will be turned to a scalar subquery.

        Returns
        -------
        Scalar
            A scalar subquery or a literal

        Examples
        --------
        >>> import ibis
        >>>
        >>> ibis.options.interactive = True
        >>>
        >>> t = ibis.examples.penguins.fetch()
        >>> max_gentoo_weight = t.filter(t.species == "Gentoo").body_mass_g.max()
        >>> light_penguins = t.filter(t.body_mass_g < max_gentoo_weight / 2)
        >>> light_penguins.group_by("species").count()
        ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┓
        ┃ species   ┃ CountStar() ┃
        ┡━━━━━━━━━━━╇━━━━━━━━━━━━━┩
        │ string    │ int64       │
        ├───────────┼─────────────┤
        │ Adelie    │          15 │
        │ Chinstrap │           2 │
        └───────────┴─────────────┘
        """
        parents = self.op().relations
        if parents:
            return ops.ScalarSubquery(self.as_table()).to_expr()
        else:
            return self

    def as_table(self) -> ir.Table:
        """Promote the scalar expression to a table.

        Returns
        -------
        Table
            A table expression

        Examples
        --------
        Promote an aggregation to a table

        >>> import ibis
        >>> import ibis.expr.types as ir
        >>> t = ibis.table(dict(a="str"), name="t")
        >>> expr = t.a.length().sum().name("len").as_table()
        >>> isinstance(expr, ir.Table)
        True

        Promote a literal value to a table

        >>> import ibis.expr.types as ir
        >>> lit = ibis.literal(1).name("a").as_table()
        >>> isinstance(lit, ir.Table)
        True
        """
        parents = self.op().relations

        if len(parents) == 0:
            return ops.DummyTable({self.get_name(): self}).to_expr()
        elif len(parents) == 1:
            (parent,) = parents
            return parent.to_expr().aggregate(self)
        else:
            raise com.RelationError(
                f"The scalar expression {self} cannot be converted to a "
                "table expression because it involves multiple base table "
                "references"
            )

    def __deferred_repr__(self):
        return f"<scalar[{self.type()}]>"

    def _repr_html_(self) -> str | None:
        return None


@public
class Column(Value, _FixedTextJupyterMixin):
    # Higher than numpy & dask objects
    __array_priority__ = 20

    __array_ufunc__ = None

    def __getitem__(self, _):
        raise TypeError(
            f"{self.__class__.__name__!r} is not subscriptable: "
            "see https://ibis-project.org/tutorial/ibis-for-pandas-users/#ibis-for-pandas-users for details."
        )

    def __array__(self, dtype=None):
        return self.execute().__array__(dtype)

    def __interactive_rich_console__(self, console, options):
        named = self.name(self.op().name)
        projection = named.as_table()
        return console.render(projection, options=options)

    def __pyarrow_result__(
        self, table: pa.Table, data_mapper: type[PyArrowData] | None = None
    ) -> pa.Array | pa.ChunkedArray:
        if data_mapper is None:
            from ibis.formats.pyarrow import PyArrowData as data_mapper

        return data_mapper.convert_column(table[0], self.type())

    def __pandas_result__(self, df: pd.DataFrame) -> pd.Series:
        from ibis.formats.pandas import PandasData

        assert (
            len(df.columns) == 1
        ), "more than one column when converting columnar result DataFrame to Series"
        # in theory we could use df.iloc[:, 0], but there seems to be a bug in
        # older geopandas where df.iloc[:, 0] doesn't return the same kind of
        # object as df.loc[:, column_name] when df is a GeoDataFrame
        #
        # the bug is that iloc[:, 0] returns a bare series whereas
        # df.loc[:, column_name] returns the special GeoSeries object.
        #
        # this bug is fixed in later versions of geopandas
        (column,) = df.columns
        return PandasData.convert_column(df.loc[:, column], self.type())

    def as_scalar(self) -> Scalar:
        """Inform ibis that the expression should be treated as a scalar.

        Creates a scalar subquery from the column expression. Since ibis cannot
        be sure that the column expression contains only one value, the column
        expression is wrapped in a scalar subquery and treated as a scalar.

        Note that the execution of the scalar subquery will fail if the column
        expression contains more than one value.

        Returns
        -------
        Scalar
            A scalar subquery

        Examples
        --------
        >>> import ibis
        >>>
        >>> ibis.options.interactive = True
        >>>
        >>> t = ibis.examples.penguins.fetch()
        >>> heavy_gentoo = t.filter(t.species == "Gentoo", t.body_mass_g > 6200)
        >>> from_that_island = t.filter(t.island == heavy_gentoo.island.as_scalar())
        >>> from_that_island.group_by("species").count()
        ┏━━━━━━━━━┳━━━━━━━━━━━━━┓
        ┃ species ┃ CountStar() ┃
        ┡━━━━━━━━━╇━━━━━━━━━━━━━┩
        │ string  │ int64       │
        ├─────────┼─────────────┤
        │ Adelie  │          44 │
        │ Gentoo  │         124 │
        └─────────┴─────────────┘
        """
        return self.as_table().as_scalar()

    def as_table(self) -> ir.Table:
        """Promote the expression to a [Table](./expression-tables.qmd#ibis.expr.types.Table).

        Returns
        -------
        Table
            A table expression

        Examples
        --------
        >>> import ibis
        >>> t = ibis.table(dict(a="str"), name="t")
        >>> expr = t.a.length().name("len").as_table()
        >>> expected = t.select(len=t.a.length())
        >>> expr.equals(expected)
        True
        """
        parents = self.op().relations
        values = {self.get_name(): self}

        if len(parents) == 0:
            return ops.DummyTable(values).to_expr()
        elif len(parents) == 1:
            (parent,) = parents
            return parent.to_expr().select(self)
        else:
            raise com.RelationError(
                f"Cannot convert {type(self)} expression involving multiple "
                "base table references to a projection"
            )

    def _bind_reduction_filter(self, where):
        rels = self.op().relations
        if isinstance(where, Deferred):
            if len(rels) == 0:
                raise com.IbisInputError(
                    "Unable to bind deferred expression to a table because "
                    "the expression doesn't depend on any tables"
                )
            elif len(rels) == 1:
                (table,) = rels
                return where.resolve(table.to_expr())
            else:
                raise com.RelationError(
                    "Cannot bind deferred expression to a table because the "
                    "expression depends on multiple tables"
                )
        else:
            return where

    def __deferred_repr__(self):
        return f"<column[{self.type()}]>"

    def approx_nunique(
        self,
        where: ir.BooleanValue | None = None,
    ) -> ir.IntegerScalar:
        """Return the approximate number of distinct elements in `self`.

        ::: {.callout-note}
        ## The result may or may not be exact

        Whether the result is an approximation depends on the backend.

        ::: {.callout-warning}
        ## Do not depend on the results being exact
        :::

        :::

        Parameters
        ----------
        where
            Filter in values when `where` is `True`

        Returns
        -------
        Scalar
            An approximate count of the distinct elements of `self`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.body_mass_g.approx_nunique()
        94
        >>> t.body_mass_g.approx_nunique(where=t.species == "Adelie")
        55
        """
        return ops.ApproxCountDistinct(
            self, where=self._bind_reduction_filter(where)
        ).to_expr()

    def approx_median(
        self,
        where: ir.BooleanValue | None = None,
    ) -> Scalar:
        """Return an approximate of the median of `self`.

        ::: {.callout-note}
        ## The result may or may not be exact

        Whether the result is an approximation depends on the backend.

        ::: {.callout-warning}
        ## Do not depend on the results being exact
        :::

        :::

        Parameters
        ----------
        where
            Filter in values when `where` is `True`

        Returns
        -------
        Scalar
            An approximation of the median of `self`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.body_mass_g.approx_median()
        4030
        >>> t.body_mass_g.approx_median(where=t.species == "Chinstrap")
        3700
        """
        return ops.ApproxMedian(
            self, where=self._bind_reduction_filter(where)
        ).to_expr()

    def mode(self, where: ir.BooleanValue | None = None) -> Scalar:
        """Return the mode of a column.

        Parameters
        ----------
        where
            Filter in values when `where` is `True`

        Returns
        -------
        Scalar
            The mode of `self`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.body_mass_g.mode()
        3800
        >>> t.body_mass_g.mode(where=(t.species == "Gentoo") & (t.sex == "male"))
        5550
        """
        return ops.Mode(self, where=self._bind_reduction_filter(where)).to_expr()

    def max(self, where: ir.BooleanValue | None = None) -> Scalar:
        """Return the maximum of a column.

        Parameters
        ----------
        where
            Filter in values when `where` is `True`

        Returns
        -------
        Scalar
            The maximum value in `self`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.body_mass_g.max()
        6300
        >>> t.body_mass_g.max(where=t.species == "Chinstrap")
        4800
        """
        return ops.Max(self, where=self._bind_reduction_filter(where)).to_expr()

    def min(self, where: ir.BooleanValue | None = None) -> Scalar:
        """Return the minimum of a column.

        Parameters
        ----------
        where
            Filter in values when `where` is `True`

        Returns
        -------
        Scalar
            The minimum value in `self`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.body_mass_g.min()
        2700
        >>> t.body_mass_g.min(where=t.species == "Adelie")
        2850
        """
        return ops.Min(self, where=self._bind_reduction_filter(where)).to_expr()

    def argmax(self, key: ir.Value, where: ir.BooleanValue | None = None) -> Scalar:
        """Return the value of `self` that maximizes `key`.

        Parameters
        ----------
        key
            Key to use for `max` computation.
        where
            Keep values when `where` is `True`

        Returns
        -------
        Scalar
            The value of `self` that maximizes `key`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.species.argmax(t.body_mass_g)
        'Gentoo'
        >>> t.species.argmax(t.body_mass_g, where=t.island == "Dream")
        'Chinstrap'
        """
        return ops.ArgMax(
            self, key=key, where=self._bind_reduction_filter(where)
        ).to_expr()

    def argmin(self, key: ir.Value, where: ir.BooleanValue | None = None) -> Scalar:
        """Return the value of `self` that minimizes `key`.

        Parameters
        ----------
        key
            Key to use for `min` computation.
        where
            Keep values when `where` is `True`

        Returns
        -------
        Scalar
            The value of `self` that minimizes `key`

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.species.argmin(t.body_mass_g)
        'Chinstrap'

        >>> t.species.argmin(t.body_mass_g, where=t.island == "Biscoe")
        'Adelie'
        """
        return ops.ArgMin(
            self, key=key, where=self._bind_reduction_filter(where)
        ).to_expr()

    def median(self, where: ir.BooleanValue | None = None) -> Scalar:
        """Return the median of the column.

        Parameters
        ----------
        where
            Optional boolean expression. If given, only the values where
            `where` evaluates to true will be considered for the median.

        Returns
        -------
        Scalar
            Median of the column

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()

        Compute the median of `bill_depth_mm`

        >>> t.bill_depth_mm.median()
        17.3
        >>> t.group_by(t.species).agg(median_bill_depth=t.bill_depth_mm.median()).order_by(
        ...     ibis.desc("median_bill_depth")
        ... )
        ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
        ┃ species   ┃ median_bill_depth ┃
        ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
        │ string    │ float64           │
        ├───────────┼───────────────────┤
        │ Chinstrap │             18.45 │
        │ Adelie    │             18.40 │
        │ Gentoo    │             15.00 │
        └───────────┴───────────────────┘

        In addition to numeric types, any orderable non-numeric types such as
        strings and dates work with `median`.

        >>> t.group_by(t.island).agg(median_species=t.species.median()).order_by(
        ...     ibis.desc("median_species")
        ... )
        ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
        ┃ island    ┃ median_species ┃
        ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
        │ string    │ string         │
        ├───────────┼────────────────┤
        │ Biscoe    │ Gentoo         │
        │ Dream     │ Chinstrap      │
        │ Torgersen │ Adelie         │
        └───────────┴────────────────┘
        """
        return ops.Median(self, where=where).to_expr()

    def quantile(
        self,
        quantile: float | ir.NumericValue | Sequence[ir.NumericValue | float],
        where: ir.BooleanValue | None = None,
    ) -> Scalar:
        """Return value at the given quantile.

        The output of this method is a continuous quantile if the input is
        numeric, otherwise the output is a discrete quantile.

        Parameters
        ----------
        quantile
            `0 <= quantile <= 1`, or an array of such values
            indicating the quantile or quantiles to compute
        where
            Boolean filter for input values

        Returns
        -------
        Scalar
            Quantile of the input

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()

        Compute the 99th percentile of `bill_depth`

        >>> t.bill_depth_mm.quantile(0.99)
        21.1
        >>> t.group_by(t.species).agg(p99_bill_depth=t.bill_depth_mm.quantile(0.99)).order_by(
        ...     ibis.desc("p99_bill_depth")
        ... )
        ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
        ┃ species   ┃ p99_bill_depth ┃
        ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
        │ string    │ float64        │
        ├───────────┼────────────────┤
        │ Adelie    │         21.200 │
        │ Chinstrap │         20.733 │
        │ Gentoo    │         17.256 │
        └───────────┴────────────────┘

        In addition to numeric types, any orderable non-numeric types such as
        strings and dates work with `quantile`.

        Let's compute the 99th percentile of the `species` column

        >>> t.group_by(t.island).agg(p99_species=t.species.quantile(0.99)).order_by(
        ...     ibis.desc("p99_species")
        ... )
        ┏━━━━━━━━━━━┳━━━━━━━━━━━━━┓
        ┃ island    ┃ p99_species ┃
        ┡━━━━━━━━━━━╇━━━━━━━━━━━━━┩
        │ string    │ string      │
        ├───────────┼─────────────┤
        │ Biscoe    │ Gentoo      │
        │ Dream     │ Chinstrap   │
        │ Torgersen │ Adelie      │
        └───────────┴─────────────┘
        """
        if isinstance(quantile, Sequence):
            op = ops.MultiQuantile
        else:
            op = ops.Quantile
        return op(self, quantile, where=where).to_expr()

    def nunique(self, where: ir.BooleanValue | None = None) -> ir.IntegerScalar:
        """Compute the number of distinct rows in an expression.

        Parameters
        ----------
        where
            Filter expression

        Returns
        -------
        IntegerScalar
            Number of distinct elements in an expression

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.examples.penguins.fetch()
        >>> t.body_mass_g.nunique()
        94
        >>> t.body_mass_g.nunique(where=t.species == "Adelie")
        55
        """
        return ops.CountDistinct(
            self, where=self._bind_reduction_filter(where)
        ).to_expr()

    def topk(
        self,
        k: int,
        by: ir.Value | None = None,
    ) -> ir.Table:
        """Return a "top k" expression.

        Parameters
        ----------
        k
            Return this number of rows
        by
            An expression. Defaults to `count`.

        Returns
        -------
        Table
            A top-k expression
        """

        from ibis.expr.analysis import find_first_base_table

        arg_table = find_first_base_table(self.op()).to_expr()

        if by is None:
            by = self.count()

        if callable(by):
            by = by(arg_table)
            by_table = arg_table
        elif isinstance(by, Value):
            by_table = find_first_base_table(by.op()).to_expr()
        else:
            raise com.IbisTypeError(f"Invalid `by` argument with type {type(by)}")

        assert by.op().name != self.op().name

        if not arg_table.equals(by_table):
            raise com.IbisError("Cross-table TopK; must provide a parent joined table")

        return (
            arg_table.aggregate(by, by=[self])
            .order_by(ibis.desc(by.get_name()))
            .limit(k)
        )

    def arbitrary(
        self,
        where: ir.BooleanValue | None = None,
        how: Literal["first", "last", "heavy"] = "first",
    ) -> Scalar:
        """Select an arbitrary value in a column.

        Parameters
        ----------
        where
            A filter expression
        how
            The method to use for selecting the element.

            * `"first"`: Select the first non-`NULL` element
            * `"last"`: Select the last non-`NULL` element
            * `"heavy"`: Select a frequently occurring value using the heavy
              hitters algorithm. `"heavy"` is only supported by Clickhouse
              backend.

        Returns
        -------
        Scalar
            An expression
        """
        return ops.Arbitrary(
            self, how=how, where=self._bind_reduction_filter(where)
        ).to_expr()

    def count(self, where: ir.BooleanValue | None = None) -> ir.IntegerScalar:
        """Compute the number of rows in an expression.

        Parameters
        ----------
        where
            Filter expression

        Returns
        -------
        IntegerScalar
            Number of elements in an expression
        """
        return ops.Count(self, where=self._bind_reduction_filter(where)).to_expr()

    def value_counts(self) -> ir.Table:
        """Compute a frequency table.

        Returns
        -------
        Table
            Frequency table expression

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"chars": char} for char in "aabcddd")
        >>> t
        ┏━━━━━━━━┓
        ┃ chars  ┃
        ┡━━━━━━━━┩
        │ string │
        ├────────┤
        │ a      │
        │ a      │
        │ b      │
        │ c      │
        │ d      │
        │ d      │
        │ d      │
        └────────┘
        >>> t.chars.value_counts().order_by("chars")
        ┏━━━━━━━━┳━━━━━━━━━━━━━┓
        ┃ chars  ┃ chars_count ┃
        ┡━━━━━━━━╇━━━━━━━━━━━━━┩
        │ string │ int64       │
        ├────────┼─────────────┤
        │ a      │           2 │
        │ b      │           1 │
        │ c      │           1 │
        │ d      │           3 │
        └────────┴─────────────┘
        """
        name = self.get_name()
        metric = _.count().name(f"{name}_count")
        return self.as_table().group_by(name).aggregate(metric)

    def first(self, where: ir.BooleanValue | None = None) -> Value:
        """Return the first value of a column.

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"chars": ["a", "b", "c", "d"]})
        >>> t
        ┏━━━━━━━━┓
        ┃ chars  ┃
        ┡━━━━━━━━┩
        │ string │
        ├────────┤
        │ a      │
        │ b      │
        │ c      │
        │ d      │
        └────────┘
        >>> t.chars.first()
        'a'
        >>> t.chars.first(where=t.chars != "a")
        'b'
        """
        return ops.First(self, where=self._bind_reduction_filter(where)).to_expr()

    def last(self, where: ir.BooleanValue | None = None) -> Value:
        """Return the last value of a column.

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"chars": ["a", "b", "c", "d"]})
        >>> t
        ┏━━━━━━━━┓
        ┃ chars  ┃
        ┡━━━━━━━━┩
        │ string │
        ├────────┤
        │ a      │
        │ b      │
        │ c      │
        │ d      │
        └────────┘
        >>> t.chars.last()
        'd'
        >>> t.chars.last(where=t.chars != "d")
        'c'
        """
        return ops.Last(self, where=self._bind_reduction_filter(where)).to_expr()

    def rank(self) -> ir.IntegerColumn:
        """Compute position of first element within each equal-value group in sorted order.

        Equivalent to SQL's `RANK()` window function.

        Returns
        -------
        Int64Column
            The min rank

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"values": [1, 2, 1, 2, 3, 2]})
        >>> t.mutate(rank=t.values.rank())
        ┏━━━━━━━━┳━━━━━━━┓
        ┃ values ┃ rank  ┃
        ┡━━━━━━━━╇━━━━━━━┩
        │ int64  │ int64 │
        ├────────┼───────┤
        │      1 │     0 │
        │      1 │     0 │
        │      2 │     2 │
        │      2 │     2 │
        │      2 │     2 │
        │      3 │     5 │
        └────────┴───────┘
        """
        return ibis.rank().over(order_by=self)

    def dense_rank(self) -> ir.IntegerColumn:
        """Position of first element within each group of equal values.

        Values are returned in sorted order and duplicate values are ignored.

        Equivalent to SQL's `DENSE_RANK()`.

        Returns
        -------
        IntegerColumn
            The rank

        Examples
        --------
        >>> import ibis
        >>> ibis.options.interactive = True
        >>> t = ibis.memtable({"values": [1, 2, 1, 2, 3, 2]})
        >>> t.mutate(rank=t.values.dense_rank())
        ┏━━━━━━━━┳━━━━━━━┓
        ┃ values ┃ rank  ┃
        ┡━━━━━━━━╇━━━━━━━┩
        │ int64  │ int64 │
        ├────────┼───────┤
        │      1 │     0 │
        │      1 │     0 │
        │      2 │     1 │
        │      2 │     1 │
        │      2 │     1 │
        │      3 │     2 │
        └────────┴───────┘
        """
        return ibis.dense_rank().over(order_by=self)

    def percent_rank(self) -> Column:
        """Return the relative rank of the values in the column."""
        return ibis.percent_rank().over(order_by=self)

    def cume_dist(self) -> Column:
        """Return the cumulative distribution over a window."""
        return ibis.cume_dist().over(order_by=self)

    def ntile(self, buckets: int | ir.IntegerValue) -> ir.IntegerColumn:
        """Return the integer number of a partitioning of the column values.

        Parameters
        ----------
        buckets
            Number of buckets to partition into
        """
        return ibis.ntile(buckets).over(order_by=self)

    def cummin(self, *, where=None, group_by=None, order_by=None) -> Column:
        """Return the cumulative min over a window."""
        return self.min(where=where).over(
            ibis.cumulative_window(group_by=group_by, order_by=order_by)
        )

    def cummax(self, *, where=None, group_by=None, order_by=None) -> Column:
        """Return the cumulative max over a window."""
        return self.max(where=where).over(
            ibis.cumulative_window(group_by=group_by, order_by=order_by)
        )

    def lag(
        self,
        offset: int | ir.IntegerValue | None = None,
        default: Value | None = None,
    ) -> Column:
        """Return the row located at `offset` rows **before** the current row.

        Parameters
        ----------
        offset
            Index of row to select
        default
            Value used if no row exists at `offset`
        """
        return ops.Lag(self, offset, default).to_expr()

    def lead(
        self,
        offset: int | ir.IntegerValue | None = None,
        default: Value | None = None,
    ) -> Column:
        """Return the row located at `offset` rows **after** the current row.

        Parameters
        ----------
        offset
            Index of row to select
        default
            Value used if no row exists at `offset`
        """
        return ops.Lead(self, offset, default).to_expr()

    def nth(self, n: int | ir.IntegerValue) -> Column:
        """Return the `n`th value (0-indexed) over a window.

        `.nth(0)` is equivalent to `.first()`. Negative will result in `NULL`.
        If the value of `n` is greater than the number of rows in the window,
        `NULL` will be returned.

        Parameters
        ----------
        n
            Desired rank value

        Returns
        -------
        Column
            The nth value over a window
        """
        return ops.NthValue(self, n).to_expr()


@public
class UnknownValue(Value):
    pass


@public
class UnknownScalar(Scalar):
    pass


@public
class UnknownColumn(Column):
    pass


@public
class NullValue(Value):
    pass


@public
class NullScalar(Scalar, NullValue, Singleton):
    pass


@public
class NullColumn(Column, NullValue):
    pass


@public
def null():
    """Create a NULL/NA scalar."""
    return ops.NULL.to_expr()


@public
def literal(value: Any, type: dt.DataType | str | None = None) -> Scalar:
    """Create a scalar expression from a Python value.

    ::: {.callout-tip}
    ## Use specific functions for arrays, structs and maps

    Ibis supports literal construction of arrays using the following
    functions:

    1. [`ibis.array`](./expression-collections.qmd#ibis.array)
    1. [`ibis.struct`](./expression-collections.qmd#ibis.struct)
    1. [`ibis.map`](./expression-collections.qmd#ibis.map)

    Constructing these types using `literal` will be deprecated in a future
    release.
    :::

    Parameters
    ----------
    value
        A Python value
    type
        An instance of [`DataType`](./datatypes.qmd#ibis.expr.datatypes.DataType) or a string
        indicating the ibis type of `value`. This parameter can be used
        in cases where ibis's type inference isn't sufficient for discovering
        the type of `value`.

    Returns
    -------
    Scalar
        An expression representing a literal value

    Examples
    --------
    Construct an integer literal

    >>> import ibis
    >>> x = ibis.literal(42)
    >>> x.type()
    Int8(nullable=True)

    Construct a `float64` literal from an `int`

    >>> y = ibis.literal(42, type="double")
    >>> y.type()
    Float64(nullable=True)

    Ibis checks for invalid types

    >>> ibis.literal("foobar", type="int64")  # quartodoc: +EXPECTED_FAILURE
    Traceback (most recent call last):
      ...
    TypeError: Value 'foobar' cannot be safely coerced to int64
    """
    if isinstance(value, Expr):
        node = value.op()
        if not isinstance(node, ops.Literal):
            raise TypeError(f"Ibis expression {value!r} is not a Literal")
        if type is None or node.dtype.castable(dt.dtype(type)):
            return value
        else:
            raise TypeError(
                f"Ibis literal {value!r} cannot be safely coerced to datatype {type}"
            )

    dtype = dt.infer(value) if type is None else dt.dtype(type)
    return ops.Literal(value, dtype=dtype).to_expr()


public(
    ValueExpr=Value,
    ScalarExpr=Scalar,
    ColumnExpr=Column,
    AnyValue=Value,
    AnyScalar=Scalar,
    AnyColumn=Column,
)
