use super::*;
use std::{collections::HashSet, convert::Infallible};

use full_moon::{
    ast::{self, Ast},
    node::Node,
    visitors::Visitor,
};
use serde::Deserialize;

#[derive(Clone, Copy, Default, Deserialize)]
pub struct MultipleStatementsConfig {
    one_line_if: OneLineIf,
}

pub struct MultipleStatementsLint {
    config: MultipleStatementsConfig,
}

#[derive(Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum OneLineIf {
    Allow,
    Deny,
    BreakReturnOnly,
}

impl Default for OneLineIf {
    fn default() -> Self {
        OneLineIf::BreakReturnOnly
    }
}

impl Rule for MultipleStatementsLint {
    type Config = MultipleStatementsConfig;
    type Error = Infallible;

    fn new(config: Self::Config) -> Result<Self, Self::Error> {
        Ok(MultipleStatementsLint { config })
    }

    fn pass(&self, ast: &Ast, _: &Context) -> Vec<Diagnostic> {
        let mut visitor = MultipleStatementsVisitor {
            config: self.config,
            ..MultipleStatementsVisitor::default()
        };

        visitor.visit_ast(&ast);

        visitor
            .positions
            .iter()
            .map(|position| {
                Diagnostic::new(
                    "multiple_statements",
                    "only one statement per line is allowed".to_owned(),
                    Label::new(*position),
                )
            })
            .collect()
    }

    fn severity(&self) -> Severity {
        Severity::Warning
    }

    fn rule_type(&self) -> RuleType {
        RuleType::Style
    }
}

#[derive(Default)]
struct MultipleStatementsVisitor {
    config: MultipleStatementsConfig,
    if_lines: HashSet<usize>,
    lines_with_stmt: HashSet<usize>,
    positions: Vec<(usize, usize)>,
}

impl MultipleStatementsVisitor {
    fn prepare_if(&mut self, if_block: &ast::If) {
        let line = if_block.then_token().end_position().unwrap().line();

        if self.config.one_line_if != OneLineIf::Deny {
            if self.config.one_line_if == OneLineIf::BreakReturnOnly
                && (if_block.block().iter_stmts().next().is_some()
                    || if_block.block().last_stmt().is_none())
            {
                return;
            }

            self.if_lines.insert(line);
        }
    }

    fn lint_stmt<'a, N: Node<'a>>(&mut self, stmt: N) {
        let line = stmt.end_position().unwrap().line();

        if self.lines_with_stmt.contains(&line) {
            let range = stmt.range().unwrap();
            self.positions.push((range.0.bytes(), range.1.bytes()));
        } else if self.if_lines.contains(&line) {
            self.if_lines.remove(&line);
        } else {
            self.lines_with_stmt.insert(line);
        }
    }
}

impl Visitor<'_> for MultipleStatementsVisitor {
    fn visit_last_stmt(&mut self, stmt: &ast::LastStmt) {
        self.lint_stmt(stmt);
    }

    fn visit_stmt(&mut self, stmt: &ast::Stmt) {
        if let ast::Stmt::If(if_block) = stmt {
            self.prepare_if(if_block);
        }

        self.lint_stmt(stmt);
    }
}

#[cfg(test)]
mod tests {
    use super::{super::test_util::test_lint, *};

    #[test]
    fn test_multiple_statements() {
        test_lint(
            MultipleStatementsLint::new(MultipleStatementsConfig::default()).unwrap(),
            "multiple_statements",
            "multiple_statements",
        );
    }

    #[test]
    fn test_one_line_if_deny() {
        test_lint(
            MultipleStatementsLint::new(MultipleStatementsConfig {
                one_line_if: OneLineIf::Deny,
            })
            .unwrap(),
            "multiple_statements",
            "one_line_if_deny",
        );
    }

    #[test]
    fn test_one_line_if_allow() {
        test_lint(
            MultipleStatementsLint::new(MultipleStatementsConfig {
                one_line_if: OneLineIf::Allow,
            })
            .unwrap(),
            "multiple_statements",
            "one_line_if_allow",
        );
    }

    #[test]
    fn test_one_line_if_break_return_only() {
        test_lint(
            MultipleStatementsLint::new(MultipleStatementsConfig {
                one_line_if: OneLineIf::BreakReturnOnly,
            })
            .unwrap(),
            "multiple_statements",
            "one_line_if_break_return_only",
        );
    }
}
