use rustpython_ast::{Location, Stmt};
use textwrap::{dedent, indent};

use crate::ast::types::Range;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckKind;
use crate::docstrings::helpers::leading_space;
use crate::isort::format_imports;
use crate::{Check, Settings, SourceCodeLocator};

fn extract_range(body: &[&Stmt]) -> Range {
    let location = body.first().unwrap().location;
    let end_location = body.last().unwrap().end_location.unwrap();
    Range {
        location,
        end_location,
    }
}

fn extract_indentation(body: &[&Stmt], locator: &SourceCodeLocator) -> String {
    let location = body.first().unwrap().location;
    let range = Range {
        location: Location::new(location.row(), 0),
        end_location: location,
    };
    let existing = locator.slice_source_code_range(&range);
    leading_space(&existing)
}

fn match_leading_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
    let location = body.first().unwrap().location;
    let range = Range {
        location: Location::new(location.row(), 0),
        end_location: location,
    };
    let prefix = locator.slice_source_code_range(&range);
    prefix.chars().any(|char| !char.is_whitespace())
}

fn match_trailing_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
    let end_location = body.last().unwrap().end_location.unwrap();
    let range = Range {
        location: end_location,
        end_location: Location::new(end_location.row() + 1, 0),
    };
    let suffix = locator.slice_source_code_range(&range);
    suffix.chars().any(|char| !char.is_whitespace())
}

/// I001
pub fn check_imports(
    body: Vec<&Stmt>,
    locator: &SourceCodeLocator,
    settings: &Settings,
    autofix: &fixer::Mode,
) -> Option<Check> {
    let range = extract_range(&body);
    let indentation = extract_indentation(&body, locator);

    // Special-cases: there's leading or trailing content in the import block.
    let has_leading_content = match_leading_content(&body, locator);
    let has_trailing_content = match_trailing_content(&body, locator);

    // Generate the sorted import block.
    let expected = format_imports(
        body,
        &(settings.line_length - indentation.len()),
        &settings.src,
        &settings.isort.known_first_party,
        &settings.isort.known_third_party,
        &settings.isort.extra_standard_library,
    );

    if has_leading_content || has_trailing_content {
        let mut check = Check::new(CheckKind::UnsortedImports, range);
        if autofix.patch() {
            let mut content = String::new();
            if has_leading_content {
                content.push('\n');
            }
            content.push_str(&indent(&expected, &indentation));
            check.amend(Fix::replacement(
                content,
                // Preserve leading prefix (but put the imports on a new line).
                if has_leading_content {
                    range.location
                } else {
                    Location::new(range.location.row(), 0)
                },
                // TODO(charlie): Preserve trailing suffixes. Right now, we strip them.
                Location::new(range.end_location.row() + 1, 0),
            ));
        }
        Some(check)
    } else {
        // Expand the span the entire range, including leading and trailing space.
        let range = Range {
            location: Location::new(range.location.row(), 0),
            end_location: Location::new(range.end_location.row() + 1, 0),
        };
        let actual = dedent(&locator.slice_source_code_range(&range));
        if actual != expected {
            let mut check = Check::new(CheckKind::UnsortedImports, range);
            if autofix.patch() {
                check.amend(Fix::replacement(
                    indent(&expected, &indentation),
                    range.location,
                    range.end_location,
                ));
            }
            Some(check)
        } else {
            None
        }
    }
}
