from sqlglot import exp


def lower_identities(expression):
    """
    Convert all unquoted identifiers to lower case.

    Assuming the schema is all lower case, this essentially makes identifiers case-insensitive.

    Example:
        >>> import sqlglot
        >>> expression = sqlglot.parse_one('SELECT Bar.A AS A FROM "Foo".Bar')
        >>> lower_identities(expression).sql()
        'SELECT bar.a AS A FROM "Foo".bar'

    Args:
        expression (sqlglot.Expression): expression to quote
    Returns:
        sqlglot.Expression: quoted expression
    """
    # We need to leave the output aliases unchanged, so the selects need special handling
    _lower_selects(expression)

    # These clauses can reference output aliases and also need special handling
    _lower_order(expression)
    _lower_having(expression)

    # We've already handled these args, so don't traverse into them
    traversed = {"expressions", "order", "having"}

    if isinstance(expression, exp.Subquery):
        # Root subquery, e.g. (SELECT A AS A FROM X) LIMIT 1
        lower_identities(expression.this)
        traversed |= {"this"}

    if isinstance(expression, exp.Union):
        # Union, e.g. SELECT A AS A FROM X UNION SELECT A AS A FROM X
        lower_identities(expression.left)
        lower_identities(expression.right)
        traversed |= {"this", "expression"}

    for k, v in expression.iter_expressions():
        if k in traversed:
            continue
        v.transform(_lower, copy=False)

    return expression


def _lower_selects(expression):
    for e in expression.expressions:
        # Leave output aliases as-is
        e.unalias().transform(_lower, copy=False)


def _lower_order(expression):
    order = expression.args.get("order")

    if not order:
        return

    output_aliases = {e.alias for e in expression.expressions if isinstance(e, exp.Alias)}

    for ordered in order.expressions:
        # Don't lower references to output aliases
        if not (
            isinstance(ordered.this, exp.Column)
            and not ordered.this.table
            and ordered.this.name in output_aliases
        ):
            ordered.transform(_lower, copy=False)


def _lower_having(expression):
    having = expression.args.get("having")

    if not having:
        return

    # Don't lower references to output aliases
    for agg in having.find_all(exp.AggFunc):
        agg.transform(_lower, copy=False)


def _lower(node):
    if isinstance(node, exp.Identifier) and not node.quoted:
        node.set("this", node.this.lower())
    return node
