use std::fmt::Write as _;
use tokio_postgres::types::ToSql;

use super::QueryBindings;
use super::TableAlias;
use crate::entity::EntityColumn;
use crate::STRING_WRITE_OK;

pub struct QueryExpr<'a>(QueryExprEnum<'a>);

enum QueryExprEnum<'a> {
    Column {
        table: &'static str,
        column: &'static str,
    },
    Value {
        value: Box<(dyn 'a + ToSql + Send + Sync)>,
    },
    Array {
        values: Vec<Box<&'a (dyn 'a + ToSql + Sync)>>,
    },
    Template {
        template: &'static str,
        args: Vec<Box<QueryExpr<'a>>>,
    },
    UnaryPrefixOp {
        op: &'static str,
        expr: Box<QueryExpr<'a>>,
    },
    UnaryPostfixOp {
        expr: Box<QueryExpr<'a>>,
        op: &'static str,
    },
    BinaryOp {
        left: Box<QueryExpr<'a>>,
        op: &'static str,
        right: Box<QueryExpr<'a>>,
    },
}

impl<'a> QueryExpr<'a> {
    pub fn add_to_query(&'a self, out: &mut String, bindings: &mut QueryBindings<'a>) {
        match &self.0 {
            QueryExprEnum::Column { table, column } => {
                write!(out, "{}.{}", table, column).expect(STRING_WRITE_OK);
            }
            QueryExprEnum::Value { value } => {
                let param_num = bindings.add(&**value);
                write!(out, "${}", param_num).expect(STRING_WRITE_OK);
            }
            QueryExprEnum::Array { values } => {
                out.write_char('(').expect(STRING_WRITE_OK);

                if !values.is_empty() {
                    let param_num = bindings.add(*values[0]);
                    write!(out, "${}", param_num).expect(STRING_WRITE_OK);

                    for val in &values[1..] {
                        out.write_str(", ").expect(STRING_WRITE_OK);
                        let param_num = bindings.add(**val);
                        write!(out, "${}", param_num).expect(STRING_WRITE_OK);
                    }
                }

                out.write_char(')').expect(STRING_WRITE_OK);
            }
            QueryExprEnum::Template { template, args } => {
                for (n, segment) in template.split('?').enumerate() {
                    if n > 0 {
                        args[n - 1].add_to_query(out, bindings);
                    }

                    out.write_str(segment).expect(STRING_WRITE_OK);
                }
            }
            QueryExprEnum::UnaryPrefixOp { op, expr } => {
                out.write_char('(').expect(STRING_WRITE_OK);
                write!(out, "{} ", op).expect(STRING_WRITE_OK);
                expr.add_to_query(out, bindings);
                out.write_char(')').expect(STRING_WRITE_OK);
            }
            QueryExprEnum::UnaryPostfixOp { expr, op } => {
                out.write_char('(').expect(STRING_WRITE_OK);
                expr.add_to_query(out, bindings);
                write!(out, " {}", op).expect(STRING_WRITE_OK);
                out.write_char(')').expect(STRING_WRITE_OK);
            }
            QueryExprEnum::BinaryOp { left, op, right } => {
                out.write_char('(').expect(STRING_WRITE_OK);
                left.add_to_query(out, bindings);
                write!(out, " {} ", op).expect(STRING_WRITE_OK);
                right.add_to_query(out, bindings);
                out.write_char(')').expect(STRING_WRITE_OK);
            }
        }
    }
}

// `Q` is a pseudo-mod. Why not use an actual mod?
pub struct Q;

impl Q {
    pub fn col(table: &impl TableAlias, column: &EntityColumn) -> Box<QueryExpr<'static>> {
        return Box::new(QueryExpr(QueryExprEnum::Column {
            table: table.alias(),
            column: column.name,
        }));
    }

    pub fn val<'a, T: 'a + ToSql + Send + Sync>(value: T) -> Box<QueryExpr<'a>> {
        return Box::new(QueryExpr(QueryExprEnum::Value {
            value: Box::new(value),
        }));
    }

    pub fn arr<'a, T: 'a + ToSql + Sync>(array: &'a [T]) -> Box<QueryExpr<'a>> {
        let values = array
            .iter()
            .map(|v| Box::<&'a (dyn 'a + ToSql + Sync)>::new(v))
            .collect::<Vec<_>>();

        return Box::new(QueryExpr(QueryExprEnum::Array { values }));
    }

    pub fn fill<'a>(template: &'static str, args: Vec<Box<QueryExpr<'a>>>) -> Box<QueryExpr<'a>> {
        let num_params = template.chars().filter(|c| c == &'?').count();
        let num_args = args.len();
        if num_args != num_params {
            panic!("Expected {num_params} template arguments, got {num_args}");
        }

        return Box::new(QueryExpr(QueryExprEnum::Template { template, args }));
    }

    pub fn pre_op<'a>(op: &'static str, expr: Box<QueryExpr<'a>>) -> Box<QueryExpr<'a>> {
        return Box::new(QueryExpr(QueryExprEnum::UnaryPrefixOp { op, expr }));
    }

    pub fn post_op<'a>(expr: Box<QueryExpr<'a>>, op: &'static str) -> Box<QueryExpr<'a>> {
        return Box::new(QueryExpr(QueryExprEnum::UnaryPostfixOp { expr, op }));
    }

    pub fn op<'a>(
        left: Box<QueryExpr<'a>>,
        op: &'static str,
        right: Box<QueryExpr<'a>>,
    ) -> Box<QueryExpr<'a>> {
        return Box::new(QueryExpr(QueryExprEnum::BinaryOp { left, op, right }));
    }
}
