use crate::internal::as2::ast::{BinaryOperator, UnaryOperator};
use crate::internal::as2::hir::visitor::{MutVisitor, walk_document, walk_expr};
use crate::internal::as2::hir::{ConstantKind, Document, Expr, ExprKind};
use std::borrow::Cow;

struct ConstantFolder {
    anything_changed: bool,
}

impl MutVisitor for ConstantFolder {
    fn visit_expr(&mut self, expr: &mut Expr) {
        walk_expr(self, expr);
        match &mut expr.value {
            ExprKind::BinaryOperator(op, left, right) => {
                if let ExprKind::Constant(left) = &left.value
                    && let ExprKind::Constant(right) = &right.value
                    && let Some(result) = evaluate_binary_operator(*op, left, right)
                {
                    expr.value = ExprKind::Constant(result);
                    self.anything_changed = true;
                }
            }
            ExprKind::UnaryOperator(op, inner) => {
                if let ExprKind::Constant(value) = &inner.value
                    && let Some(result) = evaluate_unary_operator(*op, value)
                {
                    expr.value = ExprKind::Constant(result);
                    self.anything_changed = true;
                }
            }
            ExprKind::CastToInteger(value) => {
                if let ExprKind::Constant(value) = &value.value
                    && let Some(value) = as_int(value, i32::MIN)
                {
                    expr.value = ExprKind::Constant(ConstantKind::Integer(value));
                    self.anything_changed = true;
                }
            }
            _ => {}
        }
    }
}

fn as_string(value: &'_ ConstantKind) -> Option<Cow<'_, str>> {
    match value {
        ConstantKind::String(value) => Some(Cow::Borrowed(value)),
        ConstantKind::Boolean(true) => Some(Cow::Borrowed("true")),
        ConstantKind::Boolean(false) => Some(Cow::Borrowed("false")),
        ConstantKind::Integer(value) => Some(Cow::Owned(value.to_string())),
        ConstantKind::Float(value) => Some(Cow::Owned(value.to_string())),
        _ => None,
    }
}

fn as_bool(value: &'_ ConstantKind) -> Option<bool> {
    match value {
        ConstantKind::String(_) => {
            let value = as_float(value).unwrap_or_default();
            Some(value != 0.0 && !value.is_nan())
        }
        ConstantKind::Boolean(value) => Some(*value),
        ConstantKind::Integer(value) => Some(*value != 0),
        ConstantKind::Float(value) => Some(*value != 0.0),
        _ => None,
    }
}

fn as_float(value: &'_ ConstantKind) -> Option<f64> {
    match value {
        ConstantKind::String(value) => Some(value.parse().unwrap_or(f64::NAN)),
        ConstantKind::Boolean(true) => Some(1.0),
        ConstantKind::Boolean(false) => Some(0.0),
        ConstantKind::Integer(value) => Some(*value as f64),
        ConstantKind::Float(value) => Some(*value),
        _ => None,
    }
}

fn as_int(value: &'_ ConstantKind, nan: i32) -> Option<i32> {
    match value {
        ConstantKind::String(_) => {
            let value = as_float(value).unwrap_or_default();
            Some(if value.is_nan() { nan } else { value as i32 })
        }
        ConstantKind::Boolean(true) => Some(1),
        ConstantKind::Boolean(false) => Some(0),
        ConstantKind::Integer(value) => Some(*value),
        ConstantKind::Float(value) => Some(*value as i32),
        _ => None,
    }
}

fn float_as_constant(value: f64) -> ConstantKind {
    if value.is_finite()
        && value.fract() == 0.0
        && value >= i32::MIN as f64
        && value <= i32::MAX as f64
    {
        ConstantKind::Integer(value as i32)
    } else {
        ConstantKind::Float(value)
    }
}

fn evaluate_binary_operator(
    op: BinaryOperator,
    left: &ConstantKind,
    right: &ConstantKind,
) -> Option<ConstantKind> {
    Some(match op {
        BinaryOperator::StringAdd => {
            if let (Some(left), Some(right)) = (as_string(left), as_string(right)) {
                ConstantKind::String(format!("{}{}", left, right))
            } else {
                return None;
            }
        }
        BinaryOperator::Add => {
            if matches!(left, ConstantKind::String(_)) || matches!(right, ConstantKind::String(_)) {
                if let (Some(left), Some(right)) = (as_string(left), as_string(right)) {
                    ConstantKind::String(format!("{}{}", left, right))
                } else {
                    return None;
                }
            } else if let (Some(left), Some(right)) = (as_float(left), as_float(right)) {
                float_as_constant(left + right)
            } else {
                return None;
            }
        }
        BinaryOperator::Sub => {
            if let (Some(left), Some(right)) = (as_float(left), as_float(right)) {
                float_as_constant(left - right)
            } else {
                return None;
            }
        }
        _ => return None,
    })
}

fn evaluate_unary_operator(op: UnaryOperator, value: &ConstantKind) -> Option<ConstantKind> {
    Some(match op {
        UnaryOperator::LogicalNot => {
            if let Some(value) = as_bool(value) {
                ConstantKind::Boolean(!value)
            } else {
                return None;
            }
        }
        UnaryOperator::Sub => {
            if let Some(value) = as_float(value) {
                float_as_constant(-value)
            } else {
                return None;
            }
        }
        UnaryOperator::Add => {
            if let Some(value) = as_float(value) {
                float_as_constant(value)
            } else {
                return None;
            }
        }
        UnaryOperator::BitNot => {
            if let Some(value) = as_int(value, 0) {
                ConstantKind::Integer(!value)
            } else {
                return None;
            }
        }
        _ => return None,
    })
}

pub fn fold_constants(document: &mut Document) -> bool {
    let mut folder = ConstantFolder {
        anything_changed: false,
    };

    walk_document(&mut folder, document);

    folder.anything_changed
}
