mod binary_op;
mod case;
mod exists;
mod is_null;
mod like;
mod nested;
mod unary_op;

pub mod aggregate;
pub mod alias_as;
pub mod between;
pub mod function;
pub mod in_list;
pub mod numeric;

use {
    crate::{
        ast::{Aggregate, AstLiteral, BinaryOperator, Expr, Function, Query, UnaryOperator},
        ast_builder::QueryNode,
        parse_sql::{parse_comma_separated_exprs, parse_expr, parse_query},
        prelude::DataType,
        result::{Error, Result},
        translate::{translate_expr, translate_query},
    },
    aggregate::AggregateNode,
    bigdecimal::BigDecimal,
    function::FunctionNode,
    in_list::InListNode,
    numeric::NumericNode,
    std::borrow::Cow,
};
pub use {
    case::case,
    exists::{exists, not_exists},
    nested::nested,
    unary_op::{bitwise_not, factorial, minus, not, plus},
};

#[derive(Clone, Debug)]
pub enum ExprNode<'a> {
    Expr(Cow<'a, Expr>),
    SqlExpr(Cow<'a, str>),
    Identifier(Cow<'a, str>),
    Numeric(NumericNode<'a>),
    QuotedString(Cow<'a, str>),
    TypedString {
        data_type: DataType,
        value: Cow<'a, str>,
    },
    Between {
        expr: Box<ExprNode<'a>>,
        negated: bool,
        low: Box<ExprNode<'a>>,
        high: Box<ExprNode<'a>>,
    },
    Like {
        expr: Box<ExprNode<'a>>,
        negated: bool,
        pattern: Box<ExprNode<'a>>,
    },
    ILike {
        expr: Box<ExprNode<'a>>,
        negated: bool,
        pattern: Box<ExprNode<'a>>,
    },
    BinaryOp {
        left: Box<ExprNode<'a>>,
        op: BinaryOperator,
        right: Box<ExprNode<'a>>,
    },
    UnaryOp {
        op: UnaryOperator,
        expr: Box<ExprNode<'a>>,
    },
    IsNull(Box<ExprNode<'a>>),
    IsNotNull(Box<ExprNode<'a>>),
    InList {
        expr: Box<ExprNode<'a>>,
        list: Box<InListNode<'a>>,
        negated: bool,
    },
    Nested(Box<ExprNode<'a>>),
    Function(Box<FunctionNode<'a>>),
    Aggregate(Box<AggregateNode<'a>>),
    Exists {
        subquery: Box<QueryNode<'a>>,
        negated: bool,
    },
    Subquery(Box<QueryNode<'a>>),
    Case {
        operand: Option<Box<ExprNode<'a>>>,
        when_then: Vec<(ExprNode<'a>, ExprNode<'a>)>,
        else_result: Option<Box<ExprNode<'a>>>,
    },
}

impl<'a> TryFrom<ExprNode<'a>> for Expr {
    type Error = Error;

    fn try_from(expr_node: ExprNode<'a>) -> Result<Self> {
        match expr_node {
            ExprNode::Expr(expr) => Ok(expr.into_owned()),
            ExprNode::SqlExpr(expr) => {
                let expr = parse_expr(expr)?;

                translate_expr(&expr)
            }
            ExprNode::Identifier(value) => {
                let idents = value.as_ref().split('.').collect::<Vec<_>>();

                Ok(match idents.as_slice() {
                    [alias, ident] => Expr::CompoundIdentifier {
                        alias: alias.to_string(),
                        ident: ident.to_string(),
                    },
                    _ => Expr::Identifier(value.into_owned()),
                })
            }
            ExprNode::Numeric(node) => node.try_into().map(Expr::Literal),
            ExprNode::QuotedString(value) => {
                let value = value.into_owned();

                Ok(Expr::Literal(AstLiteral::QuotedString(value)))
            }
            ExprNode::TypedString { data_type, value } => Ok(Expr::TypedString {
                data_type,
                value: value.into_owned(),
            }),
            ExprNode::Between {
                expr,
                negated,
                low,
                high,
            } => {
                let expr = Expr::try_from(*expr).map(Box::new)?;
                let low = Expr::try_from(*low).map(Box::new)?;
                let high = Expr::try_from(*high).map(Box::new)?;

                Ok(Expr::Between {
                    expr,
                    negated,
                    low,
                    high,
                })
            }
            ExprNode::Like {
                expr,
                negated,
                pattern,
            } => {
                let expr = Expr::try_from(*expr).map(Box::new)?;
                let pattern = Expr::try_from(*pattern).map(Box::new)?;

                Ok(Expr::Like {
                    expr,
                    negated,
                    pattern,
                })
            }
            ExprNode::ILike {
                expr,
                negated,
                pattern,
            } => {
                let expr = Expr::try_from(*expr).map(Box::new)?;
                let pattern = Expr::try_from(*pattern).map(Box::new)?;

                Ok(Expr::ILike {
                    expr,
                    negated,
                    pattern,
                })
            }
            ExprNode::BinaryOp { left, op, right } => {
                let left = Expr::try_from(*left).map(Box::new)?;
                let right = Expr::try_from(*right).map(Box::new)?;

                Ok(Expr::BinaryOp { left, op, right })
            }
            ExprNode::UnaryOp { op, expr } => {
                let expr = Expr::try_from(*expr).map(Box::new)?;
                Ok(Expr::UnaryOp { op, expr })
            }
            ExprNode::IsNull(expr) => Expr::try_from(*expr).map(Box::new).map(Expr::IsNull),
            ExprNode::IsNotNull(expr) => Expr::try_from(*expr).map(Box::new).map(Expr::IsNotNull),
            ExprNode::InList {
                expr,
                list,
                negated,
            } => {
                let expr = Expr::try_from(*expr).map(Box::new)?;

                match *list {
                    InListNode::InList(list) => {
                        let list = list
                            .into_iter()
                            .map(Expr::try_from)
                            .collect::<Result<Vec<_>>>()?;
                        Ok(Expr::InList {
                            expr,
                            list,
                            negated,
                        })
                    }
                    InListNode::Query(subquery) => {
                        let subquery = Query::try_from(*subquery).map(Box::new)?;
                        Ok(Expr::InSubquery {
                            expr,
                            subquery,
                            negated,
                        })
                    }
                    InListNode::Text(value) => {
                        let subquery = parse_query(value.clone())
                            .and_then(|item| translate_query(&item))
                            .map(Box::new);

                        if let Ok(subquery) = subquery {
                            return Ok(Expr::InSubquery {
                                expr,
                                subquery,
                                negated,
                            });
                        }

                        parse_comma_separated_exprs(&*value)?
                            .iter()
                            .map(translate_expr)
                            .collect::<Result<Vec<_>>>()
                            .map(|list| Expr::InList {
                                expr,
                                list,
                                negated,
                            })
                    }
                }
            }
            ExprNode::Nested(expr) => Expr::try_from(*expr).map(Box::new).map(Expr::Nested),
            ExprNode::Function(func_expr) => Function::try_from(*func_expr)
                .map(Box::new)
                .map(Expr::Function),
            ExprNode::Aggregate(aggr_expr) => Aggregate::try_from(*aggr_expr)
                .map(Box::new)
                .map(Expr::Aggregate),
            ExprNode::Exists { subquery, negated } => Query::try_from(*subquery)
                .map(Box::new)
                .map(|subquery| Expr::Exists { subquery, negated }),
            ExprNode::Subquery(subquery) => {
                Query::try_from(*subquery).map(Box::new).map(Expr::Subquery)
            }
            ExprNode::Case {
                operand,
                when_then,
                else_result,
            } => {
                let operand = operand
                    .map(|expr| Expr::try_from(*expr))
                    .transpose()?
                    .map(Box::new);
                let when_then = when_then
                    .into_iter()
                    .map(|(when, then)| {
                        let when = Expr::try_from(when)?;
                        let then = Expr::try_from(then)?;
                        Ok((when, then))
                    })
                    .collect::<Result<Vec<_>>>()?;

                let else_result = else_result
                    .map(|expr| Expr::try_from(*expr))
                    .transpose()?
                    .map(Box::new);
                Ok(Expr::Case {
                    operand,
                    when_then,
                    else_result,
                })
            }
        }
    }
}

impl<'a> From<&'a str> for ExprNode<'a> {
    fn from(expr: &'a str) -> Self {
        ExprNode::SqlExpr(Cow::Borrowed(expr))
    }
}

impl<'a> From<String> for ExprNode<'a> {
    fn from(expr: String) -> Self {
        ExprNode::SqlExpr(Cow::Owned(expr))
    }
}

impl<'a> From<i64> for ExprNode<'a> {
    fn from(n: i64) -> Self {
        ExprNode::Expr(Cow::Owned(Expr::Literal(AstLiteral::Number(
            BigDecimal::from(n),
        ))))
    }
}

impl<'a> From<bool> for ExprNode<'a> {
    fn from(b: bool) -> Self {
        ExprNode::Expr(Cow::Owned(Expr::Literal(AstLiteral::Boolean(b))))
    }
}

impl<'a> From<QueryNode<'a>> for ExprNode<'a> {
    fn from(node: QueryNode<'a>) -> Self {
        ExprNode::Subquery(Box::new(node))
    }
}

impl<'a> From<Expr> for ExprNode<'a> {
    fn from(expr: Expr) -> Self {
        ExprNode::Expr(Cow::Owned(expr))
    }
}

impl<'a> From<&'a Expr> for ExprNode<'a> {
    fn from(expr: &'a Expr) -> Self {
        ExprNode::Expr(Cow::Borrowed(expr))
    }
}

pub fn expr<'a, T: Into<Cow<'a, str>>>(value: T) -> ExprNode<'a> {
    ExprNode::SqlExpr(value.into())
}

pub fn col<'a, T: Into<Cow<'a, str>>>(value: T) -> ExprNode<'a> {
    ExprNode::Identifier(value.into())
}

pub fn num<'a, T: Into<NumericNode<'a>>>(value: T) -> ExprNode<'a> {
    ExprNode::Numeric(value.into())
}

pub fn text<'a, T: Into<Cow<'a, str>>>(value: T) -> ExprNode<'a> {
    ExprNode::QuotedString(value.into())
}

pub fn date<'a, T: Into<Cow<'a, str>>>(date: T) -> ExprNode<'a> {
    ExprNode::TypedString {
        data_type: DataType::Date,
        value: date.into(),
    }
}

pub fn timestamp<'a, T: Into<Cow<'a, str>>>(timestamp: T) -> ExprNode<'a> {
    ExprNode::TypedString {
        data_type: DataType::Timestamp,
        value: timestamp.into(),
    }
}

pub fn time<'a, T: Into<Cow<'a, str>>>(time: T) -> ExprNode<'a> {
    ExprNode::TypedString {
        data_type: DataType::Time,
        value: time.into(),
    }
}

pub fn uuid<'a, T: Into<Cow<'a, str>>>(uuid: T) -> ExprNode<'a> {
    ExprNode::TypedString {
        data_type: DataType::Uuid,
        value: uuid.into(),
    }
}

/// Returns an AST ExprNode containing the provided Bytea.
///
/// # Arguments
/// * `bytea` - A byte array to be converted to a Bytea AST node.
///
pub fn bytea<'a, T: AsRef<[u8]>>(bytea: T) -> ExprNode<'a> {
    ExprNode::TypedString {
        data_type: DataType::Bytea,
        value: hex::encode(bytea).into(),
    }
}

pub fn subquery<'a, T: Into<QueryNode<'a>>>(query_node: T) -> ExprNode<'a> {
    ExprNode::Subquery(Box::new(query_node.into()))
}

pub fn null() -> ExprNode<'static> {
    ExprNode::Expr(Cow::Owned(Expr::Literal(AstLiteral::Null)))
}

#[cfg(test)]
mod tests {
    use {
        super::ExprNode,
        crate::{
            ast::Expr,
            ast_builder::{
                QueryNode, bytea, col, date, expr, null, num, subquery, table, test_expr, text,
                time, timestamp, uuid,
            },
        },
    };

    #[test]
    fn into_expr_node() {
        let actual: ExprNode = "id IS NOT NULL".into();
        let expected = "id IS NOT NULL";
        test_expr(actual, expected);

        let actual: ExprNode = String::from("1 + 10)").into();
        let expected = "1 + 10";
        test_expr(actual, expected);

        let actual: ExprNode = 1024.into();
        let expected = "1024";
        test_expr(actual, expected);

        let actual: ExprNode = true.into();
        let expected = "True";
        test_expr(actual, expected);

        let actual: ExprNode = QueryNode::from(table("Foo").select().project("id")).into();
        let expected = "(SELECT id FROM Foo)";
        test_expr(actual, expected);

        let expr = Expr::Identifier("id".to_owned());
        let actual: ExprNode = (&expr).into();
        let expected = "id";
        test_expr(actual, expected);

        let actual: ExprNode = expr.into();
        test_expr(actual, expected);
    }

    #[test]
    fn syntactic_sugar() {
        let actual = expr("col1 > 10");
        let expected = "col1 > 10";
        test_expr(actual, expected);

        let actual = col("id");
        let expected = "id";
        test_expr(actual, expected);

        let actual = col("Foo.id");
        let expected = "Foo.id";
        test_expr(actual, expected);

        let actual = num(2048);
        let expected = "2048";
        test_expr(actual, expected);

        let actual = num(6.5);
        let expected = "6.5";
        test_expr(actual, expected);

        let actual = num("123.456");
        let expected = "123.456";
        test_expr(actual, expected);

        let actual = text("hello world");
        let expected = "'hello world'";
        test_expr(actual, expected);

        let actual = date("2022-10-11");
        let expected = "DATE '2022-10-11'";
        test_expr(actual, expected);

        let actual = timestamp("2022-10-11 13:34:49");
        let expected = "TIMESTAMP '2022-10-11 13:34:49'";
        test_expr(actual, expected);

        let actual = time("15:00:07");
        let expected = "TIME '15:00:07'";
        test_expr(actual, expected);

        let actual = uuid("936DA01F9ABD4d9d80C702AF85C822A8");
        let expected = "UUID '936DA01F9ABD4d9d80C702AF85C822A8'";
        test_expr(actual, expected);

        let actual = bytea(b"hello world");
        let expected = "BYTEA '68656c6c6f20776f726c64'";
        test_expr(actual, expected);

        let actual = subquery(table("Foo").select().filter("id IS NOT NULL"));
        let expected = "(SELECT * FROM Foo WHERE id IS NOT NULL)";
        test_expr(actual, expected);

        let actual = null();
        let expected = "NULL";
        test_expr(actual, expected);
    }
}
