//! Process for compiling a single compilation unit.

use std::io::Write;
use std::path::PathBuf;
use std::process::Command;

use once_cell::sync::OnceCell;
use revive_common::deserialize_from_slice;
use revive_common::EXIT_CODE_SUCCESS;
use revive_solc_json_interface::standard_json::output::error::source_location::SourceLocation;
use revive_solc_json_interface::SolcStandardJsonOutputError;
use serde::de::DeserializeOwned;
use serde::Serialize;

use super::Input;
use super::Output;
use super::Process;

/// The overriden executable name used when the compiler is run as a library.
pub static EXECUTABLE: OnceCell<PathBuf> = OnceCell::new();

pub struct NativeProcess;

impl Process for NativeProcess {
    fn run(input: Input) -> anyhow::Result<()> {
        let source_location = SourceLocation::new(input.contract.identifier.path.to_owned());

        let result = std::thread::Builder::new()
            .stack_size(crate::RAYON_WORKER_STACK_SIZE)
            .spawn(move || {
                input
                    .contract
                    .compile(
                        input.solc_version,
                        input.optimizer_settings,
                        input.metadata_hash,
                        input.debug_config,
                        &input.llvm_arguments,
                        input.memory_config,
                        input.missing_libraries,
                        input.factory_dependencies,
                        input.identifier_paths,
                    )
                    .map(Output::new)
                    .map_err(|error| {
                        SolcStandardJsonOutputError::new_error(error, Some(source_location), None)
                    })
            })
            .expect("Threading error")
            .join()
            .expect("Threading error");

        serde_json::to_writer(std::io::stdout(), &result)
            .map_err(|error| anyhow::anyhow!("Stdout writing error: {error}"))?;

        Ok(())
    }

    fn call<I, O>(path: &str, input: I) -> Result<O, SolcStandardJsonOutputError>
    where
        I: Serialize,
        O: DeserializeOwned,
    {
        let executable = EXECUTABLE
            .get()
            .cloned()
            .unwrap_or_else(|| std::env::current_exe().expect("Should have an executable"));
        let mut command = Command::new(executable.as_path());
        command.stdin(std::process::Stdio::piped());
        command.stdout(std::process::Stdio::piped());
        command.stderr(std::process::Stdio::piped());
        command.arg("--recursive-process");
        command.arg(path);

        let mut process = command
            .spawn()
            .unwrap_or_else(|error| panic!("{executable:?} subprocess spawning: {error:?}"));
        let stdin = process
            .stdin
            .as_mut()
            .unwrap_or_else(|| panic!("{executable:?} subprocess stdin getting error"));
        let stdin_input = serde_json::to_vec(&input).expect("Always valid");
        stdin
            .write_all(stdin_input.as_slice())
            .unwrap_or_else(|error| panic!("{executable:?} subprocess stdin writing: {error:?}"));

        let result = process
            .wait_with_output()
            .unwrap_or_else(|error| panic!("{executable:?} subprocess output reading: {error:?}"));

        if result.status.code() != Some(EXIT_CODE_SUCCESS) {
            let message = format!(
                "{executable:?} subprocess failed with exit code {:?}:\n{}\n{}",
                result.status.code(),
                String::from_utf8_lossy(result.stdout.as_slice()),
                String::from_utf8_lossy(result.stderr.as_slice()),
            );
            return Err(SolcStandardJsonOutputError::new_error(
                message,
                Some(SourceLocation::new(path.to_owned())),
                None,
            ));
        }

        match deserialize_from_slice(result.stdout.as_slice()) {
            Ok(output) => output,
            Err(error) => {
                panic!(
                    "{executable:?} subprocess stdout parsing error: {error:?}\n{}\n{}",
                    String::from_utf8_lossy(result.stdout.as_slice()),
                    String::from_utf8_lossy(result.stderr.as_slice()),
                );
            }
        }
    }
}
