use crate::lint::{Hit, Rule, Severity};
use crate::visit;

pub struct RandomDeprecated;
pub struct RandomNewInLoop;
pub struct ClampManual;
pub struct SqrtOverSquared;
pub struct FloorDivision;
pub struct FmodOverModulo;
pub struct PowTwo;
pub struct VectorNormalizeManual;
pub struct UnnecessaryTonumber;
pub struct LerpManual;
pub struct AbsForSignCheck;
pub struct Vector3ZeroConstant;
pub struct CFrameIdentityConstant;
pub struct HugeComparison;
pub struct ExpOverPow;

impl Rule for RandomDeprecated {
    fn id(&self) -> &'static str { "math::random_deprecated" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, _ctx| {
            if visit::is_dot_call(call, "math", "random") || visit::is_dot_call(call, "math", "randomseed") {
                hits.push(Hit {
                    pos: visit::call_pos(call),
                    msg: "math.random() is deprecated - use Random.new():NextNumber()".into(),
                });
            }
        });
        hits
    }
}

impl Rule for RandomNewInLoop {
    fn id(&self) -> &'static str { "math::random_new_in_loop" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, ctx| {
            if ctx.in_loop && visit::is_dot_call(call, "Random", "new") {
                hits.push(Hit {
                    pos: visit::call_pos(call),
                    msg: "Random.new() in loop - create once outside the loop".into(),
                });
            }
        });
        hits
    }
}

impl Rule for ClampManual {
    fn id(&self) -> &'static str { "math::clamp_manual" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        for pos in visit::find_pattern_positions(source, "math.min(math.max(") {
            hits.push(Hit {
                pos,
                msg: "math.min(math.max(...)) - use math.clamp(x, min, max) instead".into(),
            });
        }
        for pos in visit::find_pattern_positions(source, "math.max(math.min(") {
            hits.push(Hit {
                pos,
                msg: "math.max(math.min(...)) - use math.clamp(x, min, max) instead".into(),
            });
        }
        hits
    }
}

impl Rule for SqrtOverSquared {
    fn id(&self) -> &'static str { "math::sqrt_over_squared" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, _ctx| {
            if visit::is_dot_call(call, "math", "sqrt") {
                let pos = visit::call_pos(call);
                let line_start = source[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
                let line_end = source[pos..].find('\n').map(|p| pos + p).unwrap_or(source.len());
                let line = &source[line_start..line_end];
                if line.contains(" < ") || line.contains(" > ")
                    || line.contains(" <= ") || line.contains(" >= ")
                    || line.contains(" == ") || line.contains(" ~= ")
                    || line.contains(" then")
                {
                    hits.push(Hit {
                        pos,
                        msg: "math.sqrt() in comparison - compare squared values instead to avoid sqrt".into(),
                    });
                }
            }
        });
        hits
    }
}

impl Rule for FloorDivision {
    fn id(&self) -> &'static str { "math::floor_division" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        for pos in visit::find_pattern_positions(source, "math.floor(") {
            let after = &source[pos + "math.floor(".len()..];
            if after.contains('/') {
                let paren_end = after.find(')').unwrap_or(0);
                let inside = &after[..paren_end];
                if inside.contains('/') && !inside.contains("//")  {
                    hits.push(Hit {
                        pos,
                        msg: "math.floor(a/b) - use a // b (integer division, single FASTCALL)".into(),
                    });
                }
            }
        }
        hits
    }
}

impl Rule for FmodOverModulo {
    fn id(&self) -> &'static str { "math::fmod_over_modulo" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, _ctx| {
            if visit::is_dot_call(call, "math", "fmod") {
                hits.push(Hit {
                    pos: visit::call_pos(call),
                    msg: "math.fmod(a, b) - use a % b instead (MOD/MODK single opcode vs function call)".into(),
                });
            }
        });
        hits
    }
}

impl Rule for PowTwo {
    fn id(&self) -> &'static str { "math::pow_two" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, _ctx| {
            if visit::is_dot_call(call, "math", "pow") {
                if let Some(second) = visit::nth_arg(call, 1) {
                    let txt = format!("{second}").trim().to_string();
                    if txt == "2" {
                        hits.push(Hit {
                            pos: visit::call_pos(call),
                            msg: "math.pow(x, 2) - use x * x instead (avoids function call overhead)".into(),
                        });
                    }
                }
            }
        });
        hits
    }
}

impl Rule for VectorNormalizeManual {
    fn id(&self) -> &'static str { "math::vector_normalize_manual" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        for pos in visit::find_pattern_positions(source, "/ ") {
            let after_start = pos + 2;
            let after_end = visit::ceil_char(source, (after_start + 60).min(source.len()));
            let after = &source[after_start..after_end];
            if after.contains(".Magnitude") {
                let before_start = visit::floor_char(source, pos.saturating_sub(60));
                let before = &source[before_start..pos];
                if before.ends_with(' ') || before.ends_with('(') {
                    hits.push(Hit {
                        pos,
                        msg: "v / v.Magnitude - use v.Unit for normalized vector (avoids manual division)".into(),
                    });
                }
            }
        }
        hits
    }
}

impl Rule for UnnecessaryTonumber {
    fn id(&self) -> &'static str { "math::unnecessary_tonumber" }
    fn severity(&self) -> Severity { Severity::Warn }

    fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, _ctx| {
            if !visit::is_bare_call(call, "tonumber") {
                return;
            }
            if let Some(arg) = visit::nth_arg(call, 0) {
                let txt = format!("{arg}").trim().to_string();
                if txt.parse::<f64>().is_ok() {
                    hits.push(Hit {
                        pos: visit::call_pos(call),
                        msg: "tonumber() on numeric literal is redundant".into(),
                    });
                }
            }
        });
        hits
    }
}

impl Rule for LerpManual {
    fn id(&self) -> &'static str { "math::lerp_manual" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        for pos in visit::find_pattern_positions(source, ") * ") {
            let before_start = visit::floor_char(source, pos.saturating_sub(40));
            let before = &source[before_start..pos];
            if before.contains(" - ") && before.contains("(") {
                let line_start = source[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
                let line_end = source[pos..].find('\n').map(|p| pos + p).unwrap_or(source.len());
                let line = &source[line_start..line_end];
                if line.contains(" + ") && line.contains(" - ") && line.contains(" * ") {
                    hits.push(Hit {
                        pos: line_start,
                        msg: "manual lerp pattern (a + (b - a) * t) - use Vector3:Lerp(), CFrame:Lerp(), or a dedicated lerp function".into(),
                    });
                }
            }
        }
        hits
    }
}

impl Rule for AbsForSignCheck {
    fn id(&self) -> &'static str { "math::abs_for_sign_check" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, _ctx| {
            if !visit::is_dot_call(call, "math", "abs") {
                return;
            }
            let pos = visit::call_pos(call);
            let line_start = source[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
            let line_end = source[pos..].find('\n').map(|p| pos + p).unwrap_or(source.len());
            let line = &source[line_start..line_end];
            if line.contains("> 0") || line.contains(">0") || line.contains("== 0") || line.contains("~= 0") {
                hits.push(Hit {
                    pos,
                    msg: "math.abs(x) compared to 0 - compare x directly (x ~= 0, x > 0, x < 0)".into(),
                });
            }
        });
        hits
    }
}

impl Rule for Vector3ZeroConstant {
    fn id(&self) -> &'static str { "math::vector3_zero_constant" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        let constants = [
            ("Vector3.new(0, 0, 0)", "Vector3.zero"),
            ("Vector3.new(0,0,0)", "Vector3.zero"),
            ("Vector3.new(1, 1, 1)", "Vector3.one"),
            ("Vector3.new(1,1,1)", "Vector3.one"),
        ];
        for (pattern, replacement) in &constants {
            for pos in visit::find_pattern_positions(source, pattern) {
                hits.push(Hit {
                    pos,
                    msg: format!("{pattern} - use {replacement} (pre-allocated constant, no allocation)"),
                });
            }
        }
        hits
    }
}

impl Rule for CFrameIdentityConstant {
    fn id(&self) -> &'static str { "math::cframe_identity_constant" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        for pos in visit::find_pattern_positions(source, "CFrame.new()") {
            hits.push(Hit {
                pos,
                msg: "CFrame.new() - use CFrame.identity (pre-allocated constant, no allocation)".into(),
            });
        }
        hits
    }
}

impl Rule for HugeComparison {
    fn id(&self) -> &'static str { "math::huge_comparison" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, source: &str, _ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        let loop_depth = build_loop_depth_map(source);
        let line_starts = line_start_offsets(source);
        for pos in visit::find_pattern_positions(source, "math.huge") {
            let line = line_starts.partition_point(|&s| s <= pos).saturating_sub(1);
            if line < loop_depth.len() && loop_depth[line] > 0 {
                hits.push(Hit {
                    pos,
                    msg: "math.huge in loop requires a global lookup each iteration - cache in a local: local INF = math.huge".into(),
                });
            }
        }
        hits
    }
}

impl Rule for ExpOverPow {
    fn id(&self) -> &'static str { "math::exp_over_pow" }
    fn severity(&self) -> Severity { Severity::Allow }

    fn check(&self, _source: &str, ast: &full_moon::ast::Ast) -> Vec<Hit> {
        let mut hits = Vec::new();
        visit::each_call(ast, |call, ctx| {
            if ctx.in_loop && visit::is_dot_call(call, "math", "exp") {
                hits.push(Hit {
                    pos: visit::call_pos(call),
                    msg: "math.exp() in loop - if the exponent is constant, cache the result outside: local e = math.exp(k)".into(),
                });
            }
        });
        hits
    }
}

fn line_start_offsets(source: &str) -> Vec<usize> {
    let mut starts = vec![0];
    for (i, b) in source.bytes().enumerate() {
        if b == b'\n' { starts.push(i + 1); }
    }
    starts
}

fn build_loop_depth_map(source: &str) -> Vec<u32> {
    let mut depth: u32 = 0;
    let mut depths = Vec::new();
    for line in source.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("for ") || trimmed.starts_with("while ") || trimmed.starts_with("repeat") {
            depth += 1;
        }
        depths.push(depth);
        if trimmed == "end" || trimmed.starts_with("end ") || trimmed.starts_with("until ") || trimmed == "until" {
            depth = depth.saturating_sub(1);
        }
    }
    depths
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::lint::Rule;

    fn parse(src: &str) -> full_moon::ast::Ast {
        full_moon::parse(src).unwrap()
    }

    #[test]
    fn fmod_detected() {
        let src = "local r = math.fmod(a, b)";
        let ast = parse(src);
        let hits = FmodOverModulo.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn modulo_not_flagged() {
        let src = "local r = a % b";
        let ast = parse(src);
        let hits = FmodOverModulo.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn sqrt_in_comparison_flagged() {
        let src = "if math.sqrt(x) < 10 then end";
        let ast = parse(src);
        let hits = SqrtOverSquared.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn sqrt_standalone_not_flagged() {
        let src = "local d = math.sqrt(x)";
        let ast = parse(src);
        let hits = SqrtOverSquared.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn clamp_manual_detected() {
        let src = "local c = math.min(math.max(x, 0), 1)";
        let ast = parse(src);
        let hits = ClampManual.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn clamp_not_flagged() {
        let src = "local c = math.clamp(x, 0, 1)";
        let ast = parse(src);
        let hits = ClampManual.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn floor_division_detected() {
        let src = "local r = math.floor(a / b)";
        let ast = parse(src);
        let hits = FloorDivision.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn floor_no_division_ok() {
        let src = "local r = math.floor(x)";
        let ast = parse(src);
        let hits = FloorDivision.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn random_new_in_loop_detected() {
        let src = "for i = 1, 10 do\n  local rng = Random.new()\nend";
        let ast = parse(src);
        let hits = RandomNewInLoop.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn random_new_outside_loop_ok() {
        let src = "local rng = Random.new()";
        let ast = parse(src);
        let hits = RandomNewInLoop.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn pow_two_detected() {
        let src = "local r = math.pow(x, 2)";
        let ast = parse(src);
        let hits = PowTwo.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn pow_three_not_flagged() {
        let src = "local r = math.pow(x, 3)";
        let ast = parse(src);
        let hits = PowTwo.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn vector_normalize_manual_detected() {
        let src = "local n = v / v.Magnitude";
        let ast = parse(src);
        let hits = VectorNormalizeManual.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn vector_unit_not_flagged() {
        let src = "local n = v.Unit";
        let ast = parse(src);
        let hits = VectorNormalizeManual.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn unnecessary_tonumber_detected() {
        let src = "local x = tonumber(42)";
        let ast = parse(src);
        let hits = UnnecessaryTonumber.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn tonumber_on_string_ok() {
        let src = "local x = tonumber(s)";
        let ast = parse(src);
        let hits = UnnecessaryTonumber.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn lerp_manual_detected() {
        let src = "local v = a + (b - a) * t";
        let ast = parse(src);
        let hits = LerpManual.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn abs_for_sign_check_detected() {
        let src = "if math.abs(x) > 0 then end";
        let ast = parse(src);
        let hits = AbsForSignCheck.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn abs_standalone_not_flagged() {
        let src = "local a = math.abs(x)";
        let ast = parse(src);
        let hits = AbsForSignCheck.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn vector3_zero_constant_detected() {
        let src = "local v = Vector3.new(0, 0, 0)";
        let ast = parse(src);
        let hits = Vector3ZeroConstant.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn vector3_nonzero_not_flagged() {
        let src = "local v = Vector3.new(1, 2, 3)";
        let ast = parse(src);
        let hits = Vector3ZeroConstant.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn cframe_identity_detected() {
        let src = "local cf = CFrame.new()";
        let ast = parse(src);
        let hits = CFrameIdentityConstant.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn cframe_with_args_not_flagged() {
        let src = "local cf = CFrame.new(0, 5, 0)";
        let ast = parse(src);
        let hits = CFrameIdentityConstant.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn huge_comparison_in_loop_detected() {
        let src = "for i = 1, 10 do\n  if val < math.huge then end\nend";
        let ast = parse(src);
        let hits = HugeComparison.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn huge_outside_loop_ok() {
        let src = "local max = math.huge";
        let ast = parse(src);
        let hits = HugeComparison.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }

    #[test]
    fn exp_in_loop_detected() {
        let src = "for i = 1, 10 do\n  local v = math.exp(2)\nend";
        let ast = parse(src);
        let hits = ExpOverPow.check(src, &ast);
        assert_eq!(hits.len(), 1);
    }

    #[test]
    fn exp_outside_loop_ok() {
        let src = "local v = math.exp(2)";
        let ast = parse(src);
        let hits = ExpOverPow.check(src, &ast);
        assert_eq!(hits.len(), 0);
    }
}
