// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

use std::{
    cell::RefCell,
    fmt::Display,
    fs::OpenOptions,
    io::{stderr, BufRead, BufReader, BufWriter, Write},
    path::PathBuf,
    rc::Rc,
    str::FromStr,
    time::SystemTime,
};

use clap::{crate_version, value_parser, Arg, ArgAction, ArgMatches, Command};
use uucore::error::{strip_errno, UResult, USimpleError};

use crate::find::{find_main, Dependencies};

// TODO: the `localuser` and `netuser` arguments are accepted but not yet honored; handling them
// will likely involve splitting the find logic into two calls.
pub struct Config {
    find_options: String,
    local_paths: Vec<PathBuf>,
    net_paths: Vec<String>,
    prune_paths: Vec<PathBuf>,
    prune_fs: Vec<String>,
    output: PathBuf,
    db_format: DbFormat,
}

impl From<ArgMatches> for Config {
    fn from(value: ArgMatches) -> Self {
        Self {
            find_options: value
                .get_one::<String>("findoptions")
                .cloned()
                .unwrap_or_else(String::new),
            local_paths: value.get_one::<String>("localpaths").map_or_else(
                || vec![PathBuf::from("/")],
                |s| {
                    s.split_whitespace()
                        .filter_map(|s| PathBuf::from_str(s).ok())
                        .collect()
                },
            ),
            net_paths: value
                .get_one::<String>("netpaths")
                .map(|s| {
                    s.split_whitespace()
                        .map(std::borrow::ToOwned::to_owned)
                        .collect()
                })
                .unwrap_or_default(),
            prune_paths: value.get_one::<String>("prunepaths").map_or_else(
                || {
                    ["/tmp", "/usr/tmp", "/var/tmp", "/afs"]
                        .into_iter()
                        .map(PathBuf::from)
                        .collect()
                },
                |s| s.split_whitespace().map(PathBuf::from).collect(),
            ),
            prune_fs: value.get_one::<String>("prunefs").map_or_else(
                || {
                    ["nfs", "NFS", "proc"]
                        .into_iter()
                        .map(str::to_string)
                        .collect()
                },
                |s| {
                    s.split_whitespace()
                        .map(std::borrow::ToOwned::to_owned)
                        .collect()
                },
            ),
            db_format: value
                .get_one::<DbFormat>("dbformat")
                .copied()
                .unwrap_or_default(),
            output: value
                .get_one::<PathBuf>("output")
                .cloned()
                // FIXME: the default should be platform-dependent
                .unwrap_or_else(|| PathBuf::from("/usr/local/var/locatedb")),
        }
    }
}

#[derive(Debug, Clone, Copy, Default)]
pub enum DbFormat {
    #[default]
    Locate02,
}

// used for locate's --statistics
impl Display for DbFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Locate02 => f.write_str("GNU LOCATE02"),
        }
    }
}

fn uu_app() -> Command {
    Command::new("updatedb")
        .version(crate_version!())
        .arg(
            Arg::new("findoptions")
                .long("findoptions")
                .require_equals(true)
                .env("FINDOPTIONS")
                .action(ArgAction::Append),
        )
        .arg(
            Arg::new("localpaths")
                .long("localpaths")
                .require_equals(true)
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("netpaths")
                .long("netpaths")
                .require_equals(true)
                .env("NETPATHS")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("prunepaths")
                .long("prunepaths")
                .require_equals(true)
                .env("PRUNEPATHS")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("prunefs")
                .long("prunefs")
                .require_equals(true)
                .env("PRUNEFS")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("output")
                .long("output")
                .require_equals(true)
                .value_parser(value_parser!(PathBuf))
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("localuser")
                .long("localuser")
                .require_equals(true)
                .env("LOCALUSER")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("netuser")
                .long("netuser")
                .require_equals(true)
                .env("NETUSER")
                .action(ArgAction::Set),
        )
        .arg(
            Arg::new("dbformat")
                .long("dbformat")
                .require_equals(true)
                .value_parser(["LOCATE02"])
                .action(ArgAction::Set),
        )
}

// The LOCATE02 format elides bytes from the path until the first byte that differs from the
// previous entry. It keeps a running total of the prefix length, and uses 1 or 3 bytes to write
// the difference from the previous prefix length. Paths are provided in sorted order by find.
struct Frcoder<'a> {
    reader: BufReader<&'a [u8]>,
    prev: Option<Vec<u8>>,
    prefix: usize,
    ty: DbFormat,
}

impl<'a> Frcoder<'a> {
    fn new(v: &'a [u8], ty: DbFormat) -> Self {
        Self {
            reader: BufReader::new(v),
            prev: None,
            prefix: 0,
            ty,
        }
    }

    fn generate_header(&self) -> Vec<u8> {
        match self.ty {
            DbFormat::Locate02 => "\0LOCATE02\0".as_bytes().to_vec(),
        }
    }
}

impl Iterator for Frcoder<'_> {
    type Item = Vec<u8>;

    fn next(&mut self) -> Option<Self::Item> {
        let mut path = Vec::new();
        // find prints nul bytes after each path
        if self.reader.read_until(b'\0', &mut path).ok()? == 0 {
            return None;
        }

        let prefix = path
            .iter()
            .zip(self.prev.as_deref().unwrap_or_default())
            .take_while(|(a, b)| a == b)
            .count();

        let diff = prefix as i32 - self.prefix as i32;

        // if the prefix delta exceeds 0x7f, we use 0x80 to signal that the next two bytes comprise
        // the delta
        let mut out = Vec::new();
        if diff.abs() > 0x7f {
            out.push(0x80);
            out.extend((diff as i16).to_be_bytes());
        } else {
            out.push(diff as u8);
        }

        out.extend(path.iter().skip(prefix));

        self.prefix = prefix;
        self.prev = Some(path);

        Some(out)
    }
}

// capture find's stdout
struct CapturedDependencies {
    output: Rc<RefCell<dyn Write>>,
    now: SystemTime,
}

impl CapturedDependencies {
    fn new(output: Rc<RefCell<dyn Write>>) -> Self {
        Self {
            output,
            now: SystemTime::now(),
        }
    }
}

impl Dependencies for CapturedDependencies {
    fn get_output(&self) -> &RefCell<dyn Write> {
        self.output.as_ref()
    }

    fn now(&self) -> SystemTime {
        self.now
    }

    fn confirm(&self, _prompt: &str) -> bool {
        // updatedb runs non-interactively; there is nothing to confirm.
        false
    }
}

fn do_updatedb(args: &[&str]) -> UResult<()> {
    let matches = uu_app().try_get_matches_from(args)?;
    let config = Config::from(matches);

    // offload most of the logic to find. Build the argument list as discrete tokens rather than
    // a single string so that paths and options aren't re-split on whitespace and so that an empty
    // `prunefs`/`prunepaths` can't produce an invalid expression (e.g. an empty `( )` group).
    let mut tokens: Vec<String> = Vec::new();
    tokens.extend(
        config
            .local_paths
            .iter()
            .filter_map(|p| p.to_str())
            .map(String::from),
    );
    tokens.extend(config.net_paths.iter().cloned());
    // FINDOPTIONS is a single string of options, split on whitespace as GNU updatedb does
    tokens.extend(config.find_options.split_whitespace().map(String::from));

    // Each clause prunes a subtree (by filesystem type or by path); the final clause prints the
    // entry. Clauses are OR-ed together so a path that matches no prune clause gets printed.
    let mut clauses: Vec<Vec<String>> = Vec::new();
    if !config.prune_fs.is_empty() {
        let mut clause = vec![String::from("(")];
        for (i, fs) in config.prune_fs.iter().enumerate() {
            if i > 0 {
                clause.push(String::from("-or"));
            }
            clause.push(String::from("-fstype"));
            clause.push(fs.clone());
        }
        clause.push(String::from(")"));
        clause.push(String::from("-prune"));
        clauses.push(clause);
    }
    for path in config.prune_paths.iter().filter_map(|p| p.to_str()) {
        clauses.push(vec![
            String::from("-regex"),
            path.to_string(),
            String::from("-prune"),
        ]);
    }
    clauses.push(vec![String::from("-print0")]);

    for (i, clause) in clauses.into_iter().enumerate() {
        if i > 0 {
            tokens.push(String::from("-or"));
        }
        tokens.extend(clause);
    }
    tokens.push(String::from("-sorted"));

    let mut find_args = vec!["find"];
    find_args.extend(tokens.iter().map(String::as_str));

    let output = Rc::new(RefCell::new(Vec::new()));
    let deps = CapturedDependencies::new(output.clone());
    find_main(find_args.as_slice(), &deps);

    let output_path = config.output;
    let file = OpenOptions::new()
        .write(true)
        .truncate(true)
        .create(true)
        .open(&output_path)
        .map_err(|e| {
            USimpleError::new(
                1,
                format!(
                    "cannot create '{}': {}",
                    output_path.display(),
                    strip_errno(&e)
                ),
            )
        })?;
    let mut writer = BufWriter::new(file);

    // strip the trailing "(os error N)" so write failures read like the create error above
    let write_err = |e: std::io::Error| {
        USimpleError::new(
            1,
            format!(
                "error writing '{}': {}",
                output_path.display(),
                strip_errno(&e)
            ),
        )
    };

    let output = output.borrow();
    let frcoder = Frcoder::new(output.as_slice(), config.db_format);
    writer
        .write_all(&frcoder.generate_header())
        .map_err(&write_err)?;
    for v in frcoder {
        writer.write_all(v.as_slice()).map_err(&write_err)?;
    }

    writer.flush().map_err(&write_err)?;

    Ok(())
}

pub fn updatedb_main(args: &[&str]) -> i32 {
    match do_updatedb(args) {
        Ok(()) => 0,
        Err(e) => {
            let _ = writeln!(&mut stderr(), "Error: {e}");
            1
        }
    }
}
