use core::fmt::{self, Display};
use core::num::NonZero;
use core::ops::Range;
use core::slice;
use core::str::FromStr;
use rustc_lexer::{self as lexer, FrontmatterAllowed};
use std::ffi::OsStr;
use std::fs::{self, OpenOptions};
use std::io::{self, Read as _, Seek as _, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::{self, Command, Stdio};
use std::{env, thread};
use walkdir::WalkDir;

#[derive(Clone, Copy)]
pub enum ErrAction {
    Open,
    Read,
    Write,
    Create,
    Rename,
    Delete,
    Run,
}
impl ErrAction {
    fn as_str(self) -> &'static str {
        match self {
            Self::Open => "opening",
            Self::Read => "reading",
            Self::Write => "writing",
            Self::Create => "creating",
            Self::Rename => "renaming",
            Self::Delete => "deleting",
            Self::Run => "running",
        }
    }
}

#[cold]
#[track_caller]
pub fn panic_action(err: &impl Display, action: ErrAction, path: &Path) -> ! {
    panic!("error {} `{}`: {}", action.as_str(), path.display(), *err)
}

#[track_caller]
pub fn expect_action<T>(res: Result<T, impl Display>, action: ErrAction, path: impl AsRef<Path>) -> T {
    match res {
        Ok(x) => x,
        Err(ref e) => panic_action(e, action, path.as_ref()),
    }
}

/// Wrapper around `std::fs::File` which panics with a path on failure.
pub struct File<'a> {
    pub inner: fs::File,
    pub path: &'a Path,
}
impl<'a> File<'a> {
    /// Opens a file panicking on failure.
    #[track_caller]
    pub fn open(path: &'a (impl AsRef<Path> + ?Sized), options: &mut OpenOptions) -> Self {
        let path = path.as_ref();
        Self {
            inner: expect_action(options.open(path), ErrAction::Open, path),
            path,
        }
    }

    /// Opens a file if it exists, panicking on any other failure.
    #[track_caller]
    pub fn open_if_exists(path: &'a (impl AsRef<Path> + ?Sized), options: &mut OpenOptions) -> Option<Self> {
        let path = path.as_ref();
        match options.open(path) {
            Ok(inner) => Some(Self { inner, path }),
            Err(e) if e.kind() == io::ErrorKind::NotFound => None,
            Err(e) => panic_action(&e, ErrAction::Open, path),
        }
    }

    /// Opens and reads a file into a string, panicking of failure.
    #[track_caller]
    pub fn open_read_to_cleared_string<'dst>(
        path: &'a (impl AsRef<Path> + ?Sized),
        dst: &'dst mut String,
    ) -> &'dst mut String {
        Self::open(path, OpenOptions::new().read(true)).read_to_cleared_string(dst)
    }

    /// Read the entire contents of a file to the given buffer.
    #[track_caller]
    pub fn read_append_to_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String {
        expect_action(self.inner.read_to_string(dst), ErrAction::Read, self.path);
        dst
    }

    #[track_caller]
    pub fn read_to_cleared_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String {
        dst.clear();
        self.read_append_to_string(dst)
    }

    /// Replaces the entire contents of a file.
    #[track_caller]
    pub fn replace_contents(&mut self, data: &[u8]) {
        let res = match self.inner.seek(SeekFrom::Start(0)) {
            Ok(_) => match self.inner.write_all(data) {
                Ok(()) => self.inner.set_len(data.len() as u64),
                Err(e) => Err(e),
            },
            Err(e) => Err(e),
        };
        expect_action(res, ErrAction::Write, self.path);
    }
}

/// Creates a `Command` for running cargo.
#[must_use]
pub fn cargo_cmd() -> Command {
    if let Some(path) = env::var_os("CARGO") {
        Command::new(path)
    } else {
        Command::new("cargo")
    }
}

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version {
    pub major: u16,
    pub minor: u16,
}
impl FromStr for Version {
    type Err = ();
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Some(s) = s.strip_prefix("0.")
            && let Some((major, minor)) = s.split_once('.')
            && let Ok(major) = major.parse()
            && let Ok(minor) = minor.parse()
        {
            Ok(Self { major, minor })
        } else {
            Err(())
        }
    }
}
impl Version {
    /// Displays the version as a rust version. i.e. `x.y.0`
    #[must_use]
    pub fn rust_display(self) -> impl Display {
        struct X(Version);
        impl Display for X {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "{}.{}.0", self.0.major, self.0.minor)
            }
        }
        X(self)
    }

    /// Displays the version as it should appear in clippy's toml files. i.e. `0.x.y`
    #[must_use]
    pub fn toml_display(self) -> impl Display {
        struct X(Version);
        impl Display for X {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "0.{}.{}", self.0.major, self.0.minor)
            }
        }
        X(self)
    }
}

enum TomlPart<'a> {
    Table(&'a str),
    Value(&'a str, &'a str),
}

fn toml_iter(s: &str) -> impl Iterator<Item = (usize, TomlPart<'_>)> {
    let mut pos = 0;
    s.split('\n')
        .map(move |s| {
            let x = pos;
            pos += s.len() + 1;
            (x, s)
        })
        .filter_map(|(pos, s)| {
            if let Some(s) = s.strip_prefix('[') {
                s.split_once(']').map(|(name, _)| (pos, TomlPart::Table(name)))
            } else if matches!(s.bytes().next(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) {
                s.split_once('=').map(|(key, value)| (pos, TomlPart::Value(key, value)))
            } else {
                None
            }
        })
}

pub struct CargoPackage<'a> {
    pub name: &'a str,
    pub version_range: Range<usize>,
    pub not_a_platform_range: Range<usize>,
}

#[must_use]
pub fn parse_cargo_package(s: &str) -> CargoPackage<'_> {
    let mut in_package = false;
    let mut in_platform_deps = false;
    let mut name = "";
    let mut version_range = 0..0;
    let mut not_a_platform_range = 0..0;
    for (offset, part) in toml_iter(s) {
        match part {
            TomlPart::Table(name) => {
                if in_platform_deps {
                    not_a_platform_range.end = offset;
                }
                in_package = false;
                in_platform_deps = false;

                match name.trim() {
                    "package" => in_package = true,
                    "target.'cfg(NOT_A_PLATFORM)'.dependencies" => {
                        in_platform_deps = true;
                        not_a_platform_range.start = offset;
                    },
                    _ => {},
                }
            },
            TomlPart::Value(key, value) if in_package => match key.trim_end() {
                "name" => name = value.trim(),
                "version" => {
                    version_range.start = offset + (value.len() - value.trim().len()) + key.len() + 1;
                    version_range.end = offset + key.len() + value.trim_end().len() + 1;
                },
                _ => {},
            },
            TomlPart::Value(..) => {},
        }
    }
    CargoPackage {
        name,
        version_range,
        not_a_platform_range,
    }
}

pub struct ClippyInfo {
    pub path: PathBuf,
    pub version: Version,
    pub has_intellij_hook: bool,
}
impl ClippyInfo {
    #[must_use]
    pub fn search_for_manifest() -> Self {
        let mut path = env::current_dir().expect("error reading the working directory");
        let mut buf = String::new();
        loop {
            path.push("Cargo.toml");
            if let Some(mut file) = File::open_if_exists(&path, OpenOptions::new().read(true)) {
                file.read_to_cleared_string(&mut buf);
                let package = parse_cargo_package(&buf);
                if package.name == "\"clippy\"" {
                    if let Some(version) = buf[package.version_range].strip_prefix('"')
                        && let Some(version) = version.strip_suffix('"')
                        && let Ok(version) = version.parse()
                    {
                        path.pop();
                        return ClippyInfo {
                            path,
                            version,
                            has_intellij_hook: !package.not_a_platform_range.is_empty(),
                        };
                    }
                    panic!("error reading clippy version from `{}`", file.path.display());
                }
            }

            path.pop();
            assert!(
                path.pop(),
                "error finding project root, please run from inside the clippy directory"
            );
        }
    }
}

#[derive(Clone, Copy)]
pub enum UpdateStatus {
    Unchanged,
    Changed,
}
impl UpdateStatus {
    #[must_use]
    pub fn from_changed(value: bool) -> Self {
        if value { Self::Changed } else { Self::Unchanged }
    }

    #[must_use]
    pub fn is_changed(self) -> bool {
        matches!(self, Self::Changed)
    }
}

#[derive(Clone, Copy)]
pub enum UpdateMode {
    Change,
    Check,
}
impl UpdateMode {
    #[must_use]
    pub fn from_check(check: bool) -> Self {
        if check { Self::Check } else { Self::Change }
    }

    #[must_use]
    pub fn is_check(self) -> bool {
        matches!(self, Self::Check)
    }
}

#[derive(Default)]
pub struct FileUpdater {
    src_buf: String,
    dst_buf: String,
}
impl FileUpdater {
    #[track_caller]
    fn update_file_checked_inner(
        &mut self,
        tool: &str,
        mode: UpdateMode,
        path: &Path,
        update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus,
    ) {
        let mut file = File::open(path, OpenOptions::new().read(true).write(true));
        file.read_to_cleared_string(&mut self.src_buf);
        self.dst_buf.clear();
        match (mode, update(path, &self.src_buf, &mut self.dst_buf)) {
            (UpdateMode::Check, UpdateStatus::Changed) => {
                eprintln!(
                    "the contents of `{}` are out of date\nplease run `{tool}` to update",
                    path.display()
                );
                process::exit(1);
            },
            (UpdateMode::Change, UpdateStatus::Changed) => file.replace_contents(self.dst_buf.as_bytes()),
            (UpdateMode::Check | UpdateMode::Change, UpdateStatus::Unchanged) => {},
        }
    }

    #[track_caller]
    fn update_file_inner(&mut self, path: &Path, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus) {
        let mut file = File::open(path, OpenOptions::new().read(true).write(true));
        file.read_to_cleared_string(&mut self.src_buf);
        self.dst_buf.clear();
        if update(path, &self.src_buf, &mut self.dst_buf).is_changed() {
            file.replace_contents(self.dst_buf.as_bytes());
        }
    }

    #[track_caller]
    pub fn update_file_checked(
        &mut self,
        tool: &str,
        mode: UpdateMode,
        path: impl AsRef<Path>,
        update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus,
    ) {
        self.update_file_checked_inner(tool, mode, path.as_ref(), update);
    }

    #[track_caller]
    pub fn update_file(
        &mut self,
        path: impl AsRef<Path>,
        update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus,
    ) {
        self.update_file_inner(path.as_ref(), update);
    }
}

/// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters
/// were found, or the missing delimiter if not.
pub fn update_text_region(
    path: &Path,
    start: &str,
    end: &str,
    src: &str,
    dst: &mut String,
    insert: &mut impl FnMut(&mut String),
) -> UpdateStatus {
    let Some((src_start, src_end)) = src.split_once(start) else {
        panic!("`{}` does not contain `{start}`", path.display());
    };
    let Some((replaced_text, src_end)) = src_end.split_once(end) else {
        panic!("`{}` does not contain `{end}`", path.display());
    };
    dst.push_str(src_start);
    dst.push_str(start);
    let new_start = dst.len();
    insert(dst);
    let changed = dst[new_start..] != *replaced_text;
    dst.push_str(end);
    dst.push_str(src_end);
    UpdateStatus::from_changed(changed)
}

pub fn update_text_region_fn(
    start: &str,
    end: &str,
    mut insert: impl FnMut(&mut String),
) -> impl FnMut(&Path, &str, &mut String) -> UpdateStatus {
    move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert)
}

#[derive(Clone, Copy)]
pub enum Token<'a> {
    /// Matches any number of comments / doc comments.
    AnyComment,
    Ident(&'a str),
    CaptureIdent,
    LitStr,
    CaptureLitStr,
    Bang,
    CloseBrace,
    CloseBracket,
    CloseParen,
    /// This will consume the first colon even if the second doesn't exist.
    DoubleColon,
    Comma,
    Eq,
    Lifetime,
    Lt,
    Gt,
    OpenBrace,
    OpenBracket,
    OpenParen,
    Pound,
    Semi,
}

pub struct RustSearcher<'txt> {
    text: &'txt str,
    cursor: lexer::Cursor<'txt>,
    pos: u32,
    next_token: lexer::Token,
}
impl<'txt> RustSearcher<'txt> {
    #[must_use]
    #[expect(clippy::inconsistent_struct_constructor)]
    pub fn new(text: &'txt str) -> Self {
        let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes);
        Self {
            text,
            pos: 0,
            next_token: cursor.advance_token(),
            cursor,
        }
    }

    #[must_use]
    pub fn peek_text(&self) -> &'txt str {
        &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize]
    }

    #[must_use]
    pub fn peek_len(&self) -> u32 {
        self.next_token.len
    }

    #[must_use]
    pub fn peek(&self) -> lexer::TokenKind {
        self.next_token.kind
    }

    #[must_use]
    pub fn pos(&self) -> u32 {
        self.pos
    }

    #[must_use]
    pub fn at_end(&self) -> bool {
        self.next_token.kind == lexer::TokenKind::Eof
    }

    pub fn step(&mut self) {
        // `next_len` is zero for the sentinel value and the eof marker.
        self.pos += self.next_token.len;
        self.next_token = self.cursor.advance_token();
    }

    /// Consumes the next token if it matches the requested value and captures the value if
    /// requested. Returns true if a token was matched.
    fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool {
        loop {
            match (token, self.next_token.kind) {
                (_, lexer::TokenKind::Whitespace)
                | (
                    Token::AnyComment,
                    lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. },
                ) => self.step(),
                (Token::AnyComment, _) => return true,
                (Token::Bang, lexer::TokenKind::Bang)
                | (Token::CloseBrace, lexer::TokenKind::CloseBrace)
                | (Token::CloseBracket, lexer::TokenKind::CloseBracket)
                | (Token::CloseParen, lexer::TokenKind::CloseParen)
                | (Token::Comma, lexer::TokenKind::Comma)
                | (Token::Eq, lexer::TokenKind::Eq)
                | (Token::Lifetime, lexer::TokenKind::Lifetime { .. })
                | (Token::Lt, lexer::TokenKind::Lt)
                | (Token::Gt, lexer::TokenKind::Gt)
                | (Token::OpenBrace, lexer::TokenKind::OpenBrace)
                | (Token::OpenBracket, lexer::TokenKind::OpenBracket)
                | (Token::OpenParen, lexer::TokenKind::OpenParen)
                | (Token::Pound, lexer::TokenKind::Pound)
                | (Token::Semi, lexer::TokenKind::Semi)
                | (
                    Token::LitStr,
                    lexer::TokenKind::Literal {
                        kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
                        ..
                    },
                ) => {
                    self.step();
                    return true;
                },
                (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => {
                    self.step();
                    return true;
                },
                (Token::DoubleColon, lexer::TokenKind::Colon) => {
                    self.step();
                    if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) {
                        self.step();
                        return true;
                    }
                    return false;
                },
                (
                    Token::CaptureLitStr,
                    lexer::TokenKind::Literal {
                        kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. },
                        ..
                    },
                )
                | (Token::CaptureIdent, lexer::TokenKind::Ident) => {
                    **captures.next().unwrap() = self.peek_text();
                    self.step();
                    return true;
                },
                _ => return false,
            }
        }
    }

    #[must_use]
    pub fn find_token(&mut self, token: Token<'_>) -> bool {
        let mut capture = [].iter_mut();
        while !self.read_token(token, &mut capture) {
            self.step();
            if self.at_end() {
                return false;
            }
        }
        true
    }

    #[must_use]
    pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> {
        let mut res = "";
        let mut capture = &mut res;
        let mut capture = slice::from_mut(&mut capture).iter_mut();
        while !self.read_token(token, &mut capture) {
            self.step();
            if self.at_end() {
                return None;
            }
        }
        Some(res)
    }

    #[must_use]
    pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool {
        let mut captures = captures.iter_mut();
        tokens.iter().all(|&t| self.read_token(t, &mut captures))
    }
}

#[track_caller]
pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool {
    match OpenOptions::new().create_new(true).write(true).open(new_name) {
        Ok(file) => drop(file),
        Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false,
        Err(ref e) => panic_action(e, ErrAction::Create, new_name),
    }
    match fs::rename(old_name, new_name) {
        Ok(()) => true,
        Err(ref e) => {
            drop(fs::remove_file(new_name));
            // `NotADirectory` happens on posix when renaming a directory to an existing file.
            // Windows will ignore this and rename anyways.
            if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) {
                false
            } else {
                panic_action(e, ErrAction::Rename, old_name);
            }
        },
    }
}

#[track_caller]
pub fn try_rename_dir(old_name: &Path, new_name: &Path) -> bool {
    match fs::create_dir(new_name) {
        Ok(()) => {},
        Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false,
        Err(ref e) => panic_action(e, ErrAction::Create, new_name),
    }
    // Windows can't reliably rename to an empty directory.
    #[cfg(windows)]
    drop(fs::remove_dir(new_name));
    match fs::rename(old_name, new_name) {
        Ok(()) => true,
        Err(ref e) => {
            // Already dropped earlier on windows.
            #[cfg(not(windows))]
            drop(fs::remove_dir(new_name));
            // `NotADirectory` happens on posix when renaming a file to an existing directory.
            if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) {
                false
            } else {
                panic_action(e, ErrAction::Rename, old_name);
            }
        },
    }
}

#[track_caller]
pub fn run_exit_on_err(path: &(impl AsRef<Path> + ?Sized), cmd: &mut Command) {
    match expect_action(cmd.status(), ErrAction::Run, path.as_ref()).code() {
        Some(0) => {},
        Some(n) => process::exit(n),
        None => {
            eprintln!("{} killed by signal", path.as_ref().display());
            process::exit(1);
        },
    }
}

#[track_caller]
#[must_use]
pub fn run_with_output(path: &(impl AsRef<Path> + ?Sized), cmd: &mut Command) -> Vec<u8> {
    fn f(path: &Path, cmd: &mut Command) -> Vec<u8> {
        let output = expect_action(
            cmd.stdin(Stdio::null())
                .stdout(Stdio::piped())
                .stderr(Stdio::inherit())
                .output(),
            ErrAction::Run,
            path,
        );
        expect_action(output.status.exit_ok(), ErrAction::Run, path);
        output.stdout
    }
    f(path.as_ref(), cmd)
}

/// Splits an argument list across multiple `Command` invocations.
///
/// The argument list will be split into a number of batches based on
/// `thread::available_parallelism`, with `min_batch_size` setting a lower bound on the size of each
/// batch.
///
/// If the size of the arguments would exceed the system limit additional batches will be created.
pub fn split_args_for_threads(
    min_batch_size: usize,
    make_cmd: impl FnMut() -> Command,
    args: impl ExactSizeIterator<Item: AsRef<OsStr>>,
) -> impl Iterator<Item = Command> {
    struct Iter<F, I> {
        make_cmd: F,
        args: I,
        min_batch_size: usize,
        batch_size: usize,
        thread_count: usize,
    }
    impl<F, I> Iterator for Iter<F, I>
    where
        F: FnMut() -> Command,
        I: ExactSizeIterator<Item: AsRef<OsStr>>,
    {
        type Item = Command;
        fn next(&mut self) -> Option<Self::Item> {
            if self.thread_count > 1 {
                self.thread_count -= 1;
            }
            let mut cmd = (self.make_cmd)();
            let mut cmd_len = 0usize;
            for arg in self.args.by_ref().take(self.batch_size) {
                cmd.arg(arg.as_ref());
                // `+ 8` to account for the `argv` pointer on unix.
                // Windows is complicated since the arguments are first converted to UTF-16ish,
                // but this needs to account for the space between arguments and whatever additional
                // is needed to escape within an argument.
                cmd_len += arg.as_ref().len() + 8;
                cmd_len += 8;

                // Windows has a command length limit of 32767. For unix systems this is more
                // complicated since the limit includes environment variables and room needs to be
                // left to edit them once the program starts, but the total size comes from
                // `getconf ARG_MAX`.
                //
                // For simplicity we use 30000 here under a few assumptions.
                // * Individual arguments aren't super long (the final argument is still added)
                // * `ARG_MAX` is set to a reasonable amount. Basically every system will be configured way above
                //   what windows supports, but POSIX only requires `4096`.
                if cmd_len > 30000 {
                    self.batch_size = self.args.len().div_ceil(self.thread_count).max(self.min_batch_size);
                    break;
                }
            }
            (cmd_len != 0).then_some(cmd)
        }
    }
    let thread_count = thread::available_parallelism().map_or(1, NonZero::get);
    let batch_size = args.len().div_ceil(thread_count).max(min_batch_size);
    Iter {
        make_cmd,
        args,
        min_batch_size,
        batch_size,
        thread_count,
    }
}

#[track_caller]
pub fn delete_file_if_exists(path: &Path) -> bool {
    match fs::remove_file(path) {
        Ok(()) => true,
        Err(e) if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::IsADirectory) => false,
        Err(ref e) => panic_action(e, ErrAction::Delete, path),
    }
}

#[track_caller]
pub fn delete_dir_if_exists(path: &Path) {
    match fs::remove_dir_all(path) {
        Ok(()) => {},
        Err(e) if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) => {},
        Err(ref e) => panic_action(e, ErrAction::Delete, path),
    }
}

/// Walks all items excluding top-level dot files/directories and any target directories.
pub fn walk_dir_no_dot_or_target() -> impl Iterator<Item = ::walkdir::Result<::walkdir::DirEntry>> {
    WalkDir::new(".").into_iter().filter_entry(|e| {
        e.path()
            .file_name()
            .is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.'))
    })
}
