// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! A library to prefetch files on the file system to optimize startup times
//!

mod args;
mod error;
mod format;
mod replay;
mod tracer;
#[cfg(target_os = "android")]
mod arch {
    pub mod android;
}

use std::fs::File;
use std::fs::OpenOptions;
use std::io;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::string::ToString;
use std::thread;
use std::time::Duration;

#[cfg(target_os = "android")]
use log::Level;
#[cfg(target_os = "linux")]
use log::LevelFilter;

pub use args::args_from_env;
use args::OutputFormat;
pub use args::ReplayArgs;
pub use args::{DumpArgs, MainArgs, RecordArgs, SubCommands};
pub use error::Error;
pub use format::FileId;
pub use format::InodeInfo;
pub use format::Record;
pub use format::RecordsFile;
use log::info;
pub use replay::Replay;
pub use tracer::nanoseconds_since_boot;

#[cfg(target_os = "android")]
pub use arch::android::*;

/// Records prefetch data for the given configuration
pub fn record(args: &RecordArgs) -> Result<(), Error> {
    #[cfg(target_os = "android")]
    if !can_perform_record(
        &args.get_ready_path(),
        &args.get_pack_path(),
        &args.build_fingerprint_path,
    )? {
        info!("Cannot perform record -- skipping");
        return Ok(());
    }

    info!("Starting record.");

    let exclude_mount_prefix: Vec<String> =
        args.exclude_mount_prefix.iter().filter_map(|p| p.to_str().map(String::from)).collect();
    let include_mount_prefix: Vec<String> =
        args.include_mount_prefix.iter().filter_map(|p| p.to_str().map(String::from)).collect();

    let (mut tracer, exit_tx) = tracer::Tracer::create(
        args.trace_buffer_size_kib,
        args.tracing_subsystem.clone(),
        args.tracing_instance.clone(),
        args.setup_tracing,
        exclude_mount_prefix,
        include_mount_prefix,
    )?;
    let duration = Duration::from_secs(args.duration as u64);

    let thd = thread::spawn(move || {
        if !duration.is_zero() {
            info!("Record start - waiting for duration: {duration:?}");
            thread::sleep(duration);
        } else {
            #[cfg(target_os = "android")]
            wait_for_record_stop();
        }

        info!("Prefetch record exiting");
        // We want to unwrap here on failure to send this signal. Otherwise
        // tracer will continue generating huge records data.
        exit_tx.send(()).unwrap();
    });

    let mut rf = tracer.trace(args.int_path.as_ref())?;
    thd.join()
        .map_err(|_| Error::ThreadPool { error: "Failed to join timeout thread".to_string() })?;

    let path = args.get_pack_path();

    let mut out_file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(&path)
        .map_err(|source| Error::Create { source, path: path.to_str().unwrap().to_owned() })?;

    std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644))
        .map_err(|source| Error::Create { source, path: path.to_str().unwrap().to_owned() })?;

    // Write the record file
    out_file
        .write_all(&rf.add_checksum_and_serialize()?)
        .map_err(|source| Error::Write { path: path.to_str().unwrap().to_owned(), source })?;
    out_file.sync_all()?;

    // Write build-finger-print file
    #[cfg(target_os = "android")]
    write_build_fingerprint(&args.build_fingerprint_path)?;

    Ok(())
}

/// Replays prefetch data for the given configuration
pub fn replay(args: &ReplayArgs) -> Result<(), Error> {
    #[cfg(target_os = "android")]
    if !can_perform_replay(&args.path, &args.build_fingerprint_path)? {
        info!("Cannot perform replay -- exiting.");
        return Ok(());
    }

    info!("Starting replay.");
    let replay = Replay::new(args)?;
    replay.replay()
}

/// Dumps prefetch data in the human readable form
pub fn dump(args: &DumpArgs) -> Result<(), Error> {
    let reader = File::open(&args.path)
        .map_err(|source| Error::Open { source, path: args.path.to_str().unwrap().to_string() })?;
    let rf: RecordsFile =
        serde_cbor::from_reader(reader).map_err(|e| Error::Deserialize { error: e.to_string() })?;
    match args.format {
        OutputFormat::Json => println!(
            "{:#}",
            serde_json::to_string_pretty(&rf)
                .map_err(|e| Error::Serialize { error: e.to_string() })?
        ),
        OutputFormat::Csv => rf.serialize_records_to_csv(&mut io::stdout())?,
    }
    Ok(())
}

/// An alias of android_logger::Level to use log level across android and linux.
#[cfg(target_os = "android")]
pub type LogLevel = Level;

/// An alias of log::LevelFilter to use log level across android and linux.
#[cfg(not(target_os = "android"))]
pub type LogLevel = LevelFilter;

/// Convenience logging initializer that is shared between the prefetch tool and c wrapper library
#[cfg(target_os = "android")]
pub fn init_logging(_level: LogLevel) {
    android_logger::init_once(
        android_logger::Config::default().with_max_level(log::LevelFilter::Info).format(
            |f, record| {
                write!(
                    f,
                    "{} prefetch_rs: {}:{} {}: {}",
                    nanoseconds_since_boot(),
                    record.file().unwrap_or("unknown_file"),
                    record.line().unwrap_or(0),
                    record.level(),
                    record.args()
                )
            },
        ),
    )
}

/// Convenience logging initializer that is shared between the prefetch tool and c wrapper library
#[cfg(target_os = "linux")]
pub fn init_logging(level: LogLevel) {
    let mut builder = env_logger::Builder::from_default_env();

    builder
        .filter(None, level)
        .format(|buf, record| {
            writeln!(
                buf,
                "{} prefetch_rs: {}:{} {}: {}",
                nanoseconds_since_boot(),
                record.file().unwrap_or("unknown_file"),
                record.line().unwrap_or(0),
                record.level(),
                record.args()
            )
        })
        .init();
}
