//
// Syd: rock-solid application kernel
// src/parsers.rs: /proc nom parsers
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
// This file is based in part upon procinfo-rs crate which is:
//   Copyright (c) 2015 The Rust Project Developers
//   SPDX-License-Identifier: MIT
//
// SPDX-License-Identifier: GPL-3.0

//! /proc Parsers and utility functions.

use std::{
    ffi::OsStr,
    fs::File,
    io::{ErrorKind, Read},
    os::unix::ffi::OsStrExt,
};

use btoi::{btoi, btoi_radix};
use libc::{mode_t, pid_t};
use nix::errno::Errno;
use nom::{
    branch::alt,
    bytes::complete::{tag, take_until, take_while1},
    combinator::{map, map_res},
    multi::fold_many0,
    sequence::delimited,
    Finish, IResult,
};

use crate::{
    err::err2no,
    proc::{Stat, Statm, Status},
    SydSigSet,
};

/// Read all bytes in the file until EOF, placing them into `buf`.
///
/// All bytes read from this source will be written to `buf`.  If `buf` is not large enough an
/// underflow error will be returned. This function will continuously call `read` to append more
/// data to `buf` until read returns either `Ok(0)`, or an error of non-`ErrorKind::Interrupted`
/// kind.
///
/// If successful, this function will return the slice of read bytes.
///
/// # Errors
///
/// If this function encounters an error of the kind `ErrorKind::Interrupted` then the error is
/// ignored and the operation will continue.
///
/// If any other read error is encountered then this function immediately returns.  Any bytes which
/// have already been read will be written to `buf`.
///
/// If `buf` is not large enough to hold the file, an underflow error will be returned.
pub(crate) fn read_to_end<'a>(file: &mut File, buf: &'a mut [u8]) -> Result<&'a mut [u8], Errno> {
    let mut from = 0;

    loop {
        if from == buf.len() {
            return Err(Errno::EOVERFLOW); // read underflow.
        }

        #[allow(clippy::arithmetic_side_effects)]
        match file.read(&mut buf[from..]) {
            Ok(0) => return Ok(&mut buf[..from]),
            Ok(n) => from += n,
            Err(ref e) if e.kind() == ErrorKind::Interrupted => {}
            Err(e) => return Err(err2no(&e)),
        }
    }
}

/// Transforms a `nom` parse result into a `Result` with `nix::errno::Errno`.
///
/// The parser does not have to completely consume the input.
pub(crate) fn map_result<T>(result: IResult<&[u8], T>) -> nix::Result<T> {
    match result.finish() {
        Ok((_, val)) => Ok(val),
        Err(_) => Err(Errno::EINVAL),
    }
}

/// Enum to represent different lines in the status file.
enum StatusLine<'a> {
    Command(&'a OsStr),
    Umask(mode_t),
    Pid(pid_t),
    SigPendingThread(SydSigSet),
    SigPendingProcess(SydSigSet),
    SigBlocked(SydSigSet),
    SigIgnored(SydSigSet),
    SigCaught(SydSigSet),
    Skip,
}

/// Parses the `/proc/pid/status` file, extracting the relevant fields.
pub(crate) fn parse_status(input: &[u8]) -> IResult<&[u8], Status> {
    fold_many0(
        alt((
            map(parse_command, StatusLine::Command),
            map(parse_umask, StatusLine::Umask),
            map(parse_tgid, StatusLine::Pid),
            map(parse_sig_pending_thread, StatusLine::SigPendingThread),
            map(parse_sig_pending_process, StatusLine::SigPendingProcess),
            map(parse_sig_blocked, StatusLine::SigBlocked),
            map(parse_sig_ignored, StatusLine::SigIgnored),
            map(parse_sig_caught, StatusLine::SigCaught),
            map(
                delimited(take_until(&b"\n"[..]), tag(b"\n"), tag(b"")),
                |_| StatusLine::Skip,
            ),
        )),
        Status::default,
        |mut acc, line| {
            match line {
                StatusLine::Command(cmd) => acc.command = cmd.into(),
                StatusLine::Umask(umask) => acc.umask = umask,
                StatusLine::Pid(pid) => acc.pid = pid,
                StatusLine::SigPendingThread(set) => acc.sig_pending_thread = set,
                StatusLine::SigPendingProcess(set) => acc.sig_pending_process = set,
                StatusLine::SigBlocked(set) => acc.sig_blocked = set,
                StatusLine::SigIgnored(set) => acc.sig_ignored = set,
                StatusLine::SigCaught(set) => acc.sig_caught = set,
                StatusLine::Skip => {}
            }
            acc
        },
    )(input)
}

/// Parses the `/proc/pid/stat` file,
/// extracting the `tty_nr`, `num_threads`, and `startstack` fields.
pub(crate) fn parse_stat(input: &[u8]) -> IResult<&[u8], Stat> {
    let (input, _) = parse_pid(input)?;
    let (input, _) = tag(" ")(input)?;
    let (input, _) = parse_comm(input)?;
    let (input, _) = tag(" ")(input)?;
    let (input, _) = skip_fields(4)(input)?; // Fields 3-6
    let (input, tty_nr) = parse_tty_nr(input)?;
    let (input, _) = tag(" ")(input)?;
    let (input, _) = skip_fields(12)(input)?; // Fields 8-19
    let (input, num_threads) = parse_num_threads(input)?;
    let (input, _) = tag(" ")(input)?;
    let (input, _) = skip_fields(7)(input)?; // Fields 21-27
    let (input, startstack) = parse_startstack(input)?;

    Ok((
        input,
        Stat {
            num_threads,
            startstack,
            tty_nr,
        },
    ))
}

/// Parses the `/proc/[pid]/statm` file,
/// extracting only the `size` field.
pub(crate) fn parse_statm(input: &[u8]) -> IResult<&[u8], Statm> {
    let (input, size) = parse_u64_decimal(input)?;
    Ok((input, Statm { size }))
}

/// Skips a specified number of space-separated fields.
fn skip_fields<'a>(n: usize) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], ()> {
    move |input: &[u8]| {
        let mut current_input = input;
        for _ in 0..n {
            let (i, _) = take_while1(|c| c != b' ')(current_input)?;
            current_input = i;
            let (i, _) = tag(" ")(current_input)?;
            current_input = i;
        }
        Ok((current_input, ()))
    }
}

/// Parses the "comm" field (executable name) from `/proc/pid/stat`.
fn parse_comm(input: &[u8]) -> IResult<&[u8], &OsStr> {
    delimited(tag("("), take_until(")"), tag(")"))(input).map(|(i, s)| (i, OsStr::from_bytes(s)))
}

/// Parses the "tty_nr" field from `/proc/pid/stat`.
#[inline(always)]
fn parse_tty_nr(input: &[u8]) -> IResult<&[u8], i32> {
    parse_i32_decimal(input)
}

/// Parses the "num_threads" field from `/proc/pid/stat`.
#[inline(always)]
fn parse_num_threads(input: &[u8]) -> IResult<&[u8], u64> {
    parse_u64_decimal(input)
}

/// Parses the "startstack" field from `/proc/pid/stat`.
#[inline(always)]
fn parse_startstack(input: &[u8]) -> IResult<&[u8], u64> {
    parse_u64_decimal(input)
}

/// Parses the "Name" field from `/proc/pid/status`.
fn parse_command(input: &[u8]) -> IResult<&[u8], &OsStr> {
    delimited(tag(b"Name:\t"), parse_line, tag(b"\n"))(input)
}

/// Parses a line into a `&OsStr` without heap allocations.
fn parse_line(input: &[u8]) -> IResult<&[u8], &OsStr> {
    map_res(take_until(&b"\n"[..]), |bytes: &[u8]| {
        Ok::<&OsStr, Errno>(OsStr::from_bytes(bytes))
    })(input)
}

/// Parses the "Umask" field from `/proc/pid/status`.
fn parse_umask(input: &[u8]) -> IResult<&[u8], mode_t> {
    delimited(tag(b"Umask:\t"), parse_mode, tag(b"\n"))(input)
}

/// Parses the "SigPnd" field from `/proc/pid/status`.
fn parse_sig_pending_thread(input: &[u8]) -> IResult<&[u8], SydSigSet> {
    delimited(
        tag(b"SigPnd:\t"),
        map_res(
            take_while1(|c: u8| c.is_ascii_hexdigit()),
            |bytes: &[u8]| {
                btoi_radix::<u64>(bytes, 16)
                    .map(SydSigSet::new)
                    .map_err(|_| {
                        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
                    })
            },
        ),
        tag(b"\n"),
    )(input)
}

/// Parses the "ShdPnd" field from `/proc/pid/status`.
fn parse_sig_pending_process(input: &[u8]) -> IResult<&[u8], SydSigSet> {
    delimited(
        tag(b"ShdPnd:\t"),
        map_res(
            take_while1(|c: u8| c.is_ascii_hexdigit()),
            |bytes: &[u8]| {
                btoi_radix::<u64>(bytes, 16)
                    .map(SydSigSet::new)
                    .map_err(|_| {
                        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
                    })
            },
        ),
        tag(b"\n"),
    )(input)
}

/// Parses the "SigBlk" field from `/proc/pid/status`.
fn parse_sig_blocked(input: &[u8]) -> IResult<&[u8], SydSigSet> {
    delimited(
        tag(b"SigBlk:\t"),
        map_res(
            take_while1(|c: u8| c.is_ascii_hexdigit()),
            |bytes: &[u8]| {
                btoi_radix::<u64>(bytes, 16)
                    .map(SydSigSet::new)
                    .map_err(|_| {
                        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
                    })
            },
        ),
        tag(b"\n"),
    )(input)
}

/// Parses the "SigIgn" field from `/proc/pid/status`.
fn parse_sig_ignored(input: &[u8]) -> IResult<&[u8], SydSigSet> {
    delimited(
        tag(b"SigIgn:\t"),
        map_res(
            take_while1(|c: u8| c.is_ascii_hexdigit()),
            |bytes: &[u8]| {
                btoi_radix::<u64>(bytes, 16)
                    .map(SydSigSet::new)
                    .map_err(|_| {
                        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
                    })
            },
        ),
        tag(b"\n"),
    )(input)
}

/// Parses the "SigCgt" field from `/proc/pid/status`.
fn parse_sig_caught(input: &[u8]) -> IResult<&[u8], SydSigSet> {
    delimited(
        tag(b"SigCgt:\t"),
        map_res(
            take_while1(|c: u8| c.is_ascii_hexdigit()),
            |bytes: &[u8]| {
                btoi_radix::<u64>(bytes, 16)
                    .map(SydSigSet::new)
                    .map_err(|_| {
                        nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
                    })
            },
        ),
        tag(b"\n"),
    )(input)
}

/// Parses the "Tgid" field from `/proc/pid/status`.
fn parse_tgid(input: &[u8]) -> IResult<&[u8], pid_t> {
    delimited(tag(b"Tgid:\t"), parse_pid, tag(b"\n"))(input)
}

/// Parses a `pid_t`.
#[inline(always)]
fn parse_pid(input: &[u8]) -> IResult<&[u8], pid_t> {
    parse_i32_decimal(input)
}

/// Parses a `mode_t` in base-8 format.
fn parse_mode(input: &[u8]) -> IResult<&[u8], mode_t> {
    map_res(
        take_while1(|c: u8| b"01234567".contains(&c)),
        |bytes: &[u8]| {
            btoi_radix::<mode_t>(bytes, 8).map_err(|_| {
                nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
            })
        },
    )(input)
}

/// Parses a `u64`.
fn parse_u64_decimal(input: &[u8]) -> IResult<&[u8], u64> {
    map_res(
        take_while1(|c: u8| b"0123456789".contains(&c)),
        |bytes: &[u8]| {
            btoi::<u64>(bytes).map_err(|_| {
                nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
            })
        },
    )(input)
}

/// Parses a `i32`.
fn parse_i32_decimal(input: &[u8]) -> IResult<&[u8], i32> {
    map_res(
        take_while1(|c: u8| b"0123456789".contains(&c)),
        |bytes: &[u8]| {
            btoi::<i32>(bytes).map_err(|_| {
                nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
            })
        },
    )(input)
}
