// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the snarkVM library.

// The snarkVM library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// The snarkVM library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with the snarkVM library. If not, see <https://www.gnu.org/licenses/>.

mod build;
mod clean;
mod deploy;
mod is_build_required;
mod run;

pub use build::{BuildRequest, BuildResponse};
pub use deploy::{DeployRequest, DeployResponse};

use crate::{
    file::{AVMFile, AleoFile, Manifest, ProverFile, VerifierFile, README},
    prelude::{
        Deserialize,
        Deserializer,
        Identifier,
        Locator,
        Network,
        PrivateKey,
        ProgramID,
        Response,
        Serialize,
        SerializeStruct,
        Serializer,
        Value,
    },
    synthesizer::{CallOperator, Execution, Inclusion, Instruction, Process, Program, ProvingKey, VerifyingKey},
};

use anyhow::{bail, ensure, Error, Result};
use core::str::FromStr;
use rand::{CryptoRng, Rng};
use std::path::{Path, PathBuf};

#[cfg(feature = "aleo-cli")]
use colored::Colorize;

pub struct Package<N: Network> {
    /// The program ID.
    program_id: ProgramID<N>,
    /// The directory path.
    directory: PathBuf,
    /// The manifest file.
    manifest_file: Manifest<N>,
    /// The program file.
    program_file: AleoFile<N>,
}

impl<N: Network> Package<N> {
    /// Creates a new package, at the given directory with the given program name.
    pub fn create(directory: &Path, program_id: &ProgramID<N>) -> Result<Self> {
        // Ensure the directory path does not exist.
        ensure!(!directory.exists(), "The program directory already exists: {}", directory.display());
        // Ensure the program name is valid.
        ensure!(!Program::is_reserved_keyword(program_id.name()), "Program name is invalid (reserved): {program_id}");

        // Create the program directory.
        if !directory.exists() {
            std::fs::create_dir_all(directory)?;
        }

        // Create the manifest file.
        let manifest_file = Manifest::create(directory, program_id)?;
        // Create the program file.
        let program_file = AleoFile::create(directory, program_id, true)?;
        // Create the README file.
        let _readme_file = README::create::<N>(directory, program_id)?;

        Ok(Self { program_id: *program_id, directory: directory.to_path_buf(), manifest_file, program_file })
    }

    /// Opens the package at the given directory with the given program name.
    pub fn open(directory: &Path) -> Result<Self> {
        // Ensure the directory path exists.
        ensure!(directory.exists(), "The program directory does not exist: {}", directory.display());
        // Ensure the manifest file exists.
        ensure!(
            Manifest::<N>::exists_at(directory),
            "Missing '{}' at '{}'",
            Manifest::<N>::file_name(),
            directory.display()
        );
        // Ensure the main program file exists.
        ensure!(
            AleoFile::<N>::main_exists_at(directory),
            "Missing '{}' at '{}'",
            AleoFile::<N>::main_file_name(),
            directory.display()
        );

        // Open the manifest file.
        let manifest_file = Manifest::open(directory)?;
        // Retrieve the program ID.
        let program_id = *manifest_file.program_id();
        // Ensure the program name is valid.
        ensure!(!Program::is_reserved_keyword(program_id.name()), "Program name is invalid (reserved): {program_id}");

        // Open the program file.
        let program_file = AleoFile::open(directory, &program_id, true)?;

        Ok(Self { program_id, directory: directory.to_path_buf(), manifest_file, program_file })
    }

    /// Returns the program ID.
    pub const fn program_id(&self) -> &ProgramID<N> {
        &self.program_id
    }

    /// Returns the directory path.
    pub const fn directory(&self) -> &PathBuf {
        &self.directory
    }

    /// Returns the manifest file.
    pub const fn manifest_file(&self) -> &Manifest<N> {
        &self.manifest_file
    }

    /// Returns the program file.
    pub const fn program_file(&self) -> &AleoFile<N> {
        &self.program_file
    }

    /// Returns the program.
    pub const fn program(&self) -> &Program<N> {
        self.program_file.program()
    }

    /// Returns the build directory.
    pub fn build_directory(&self) -> PathBuf {
        self.directory.join("build")
    }

    /// Returns the imports directory.
    pub fn imports_directory(&self) -> PathBuf {
        self.directory.join("imports")
    }

    /// Returns a new process for the package.
    pub fn get_process(&self) -> Result<Process<N>> {
        // Create the process.
        let mut process = Process::load()?;

        // Prepare the imports directory.
        let imports_directory = self.imports_directory();

        // Add all import programs (in order) to the process.
        self.program().imports().keys().try_for_each(|program_id| {
            // Open the Aleo program file.
            let import_program_file = AleoFile::open(&imports_directory, program_id, false)?;
            // Add the import program.
            process.add_program(import_program_file.program())?;
            Ok::<_, Error>(())
        })?;

        // Add the program to the process.
        process.add_program(self.program())?;

        Ok(process)
    }
}

#[cfg(test)]
pub(crate) mod test_helpers {
    use super::*;
    use snarkvm_console::{account::Address, network::Testnet3, prelude::TestRng};

    use std::{fs::File, io::Write};

    type CurrentNetwork = Testnet3;

    fn temp_dir() -> PathBuf {
        tempfile::tempdir().expect("Failed to open temporary directory").into_path()
    }

    /// Samples a (temporary) package containing a main program.
    pub(crate) fn sample_package() -> (PathBuf, Package<CurrentNetwork>) {
        // Initialize a temporary directory.
        let directory = temp_dir();

        // Initialize the program ID.
        let program_id = ProgramID::<CurrentNetwork>::from_str("token.aleo").unwrap();

        // Initialize the program.
        let program_string = format!(
            "
program {program_id};

record token:
    owner as address.private;
    gates as u64.private;
    amount as u64.private;

function mint:
    input r0 as address.private;
    input r1 as u64.private;
    cast r0 0u64 r1 into r2 as token.record;
    output r2 as token.record;

function transfer:
    input r0 as token.record;
    input r1 as address.private;
    input r2 as u64.private;
    sub r0.amount r2 into r3;
    cast r1 0u64 r2 into r4 as token.record;
    cast r0.owner r0.gates r3 into r5 as token.record;
    output r4 as token.record;
    output r5 as token.record;"
        );

        // Write the program string to a file in the temporary directory.
        let main_filepath = directory.join("main.aleo");
        let mut file = File::create(main_filepath).unwrap();
        file.write_all(program_string.as_bytes()).unwrap();

        // Create the manifest file.
        let _manifest_file = Manifest::create(&directory, &program_id).unwrap();

        // Open the package at the temporary directory.
        let package = Package::<Testnet3>::open(&directory).unwrap();
        assert_eq!(package.program_id(), &program_id);

        // Return the temporary directory and the package.
        (directory, package)
    }

    /// Samples a (temporary) package containing a main program and an imported program.
    pub(crate) fn sample_package_with_import() -> (PathBuf, Package<CurrentNetwork>) {
        // Initialize a temporary directory.
        let directory = temp_dir();

        // Initialize the imported program ID.
        let imported_program_id = ProgramID::<CurrentNetwork>::from_str("token.aleo").unwrap();
        // Initialize the imported program.
        let imported_program = Program::<CurrentNetwork>::from_str(&format!(
            "
program {imported_program_id};

record token:
    owner as address.private;
    gates as u64.private;
    amount as u64.private;

function mint:
    input r0 as address.private;
    input r1 as u64.private;
    cast r0 0u64 r1 into r2 as token.record;
    output r2 as token.record;

function transfer:
    input r0 as token.record;
    input r1 as address.private;
    input r2 as u64.private;
    sub r0.amount r2 into r3;
    cast r1 0u64 r2 into r4 as token.record;
    cast r0.owner r0.gates r3 into r5 as token.record;
    output r4 as token.record;
    output r5 as token.record;"
        ))
        .unwrap();

        // Create the imports directory.
        let imports_directory = directory.join("imports");
        std::fs::create_dir_all(&imports_directory).unwrap();

        // Write the imported program string to an imports file in the temporary directory.
        let import_filepath = imports_directory.join(imported_program_id.to_string());
        let mut file = File::create(import_filepath).unwrap();
        file.write_all(imported_program.to_string().as_bytes()).unwrap();

        // Initialize the main program ID.
        let main_program_id = ProgramID::<CurrentNetwork>::from_str("wallet.aleo").unwrap();
        // Initialize the main program.
        let main_program = Program::<CurrentNetwork>::from_str(&format!(
            "
import token.aleo;

program {main_program_id};

function transfer:
    input r0 as token.aleo/token.record;
    input r1 as address.private;
    input r2 as u64.private;
    call token.aleo/transfer r0 r1 r2 into r3 r4;
    output r3 as token.aleo/token.record;
    output r4 as token.aleo/token.record;"
        ))
        .unwrap();

        // Write the main program string to a file in the temporary directory.
        let main_filepath = directory.join("main.aleo");
        let mut file = File::create(main_filepath).unwrap();
        file.write_all(main_program.to_string().as_bytes()).unwrap();

        // Create the manifest file.
        let _manifest_file = Manifest::create(&directory, &main_program_id).unwrap();

        // Open the package at the temporary directory.
        let package = Package::<Testnet3>::open(&directory).unwrap();
        assert_eq!(package.program_id(), &main_program_id);

        // Return the temporary directory and the package.
        (directory, package)
    }

    /// Samples a candidate input to execute the sample package.
    pub(crate) fn sample_package_run(
        program_id: &ProgramID<CurrentNetwork>,
    ) -> (PrivateKey<CurrentNetwork>, Identifier<CurrentNetwork>, Vec<Value<CurrentNetwork>>) {
        // Initialize an RNG.
        let rng = &mut TestRng::default();

        match program_id.to_string().as_str() {
            "token.aleo" => {
                // Sample a random private key.
                let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
                let caller = Address::try_from(&private_key).unwrap();

                // Initialize the function name.
                let function_name = Identifier::from_str("mint").unwrap();

                // Initialize the function inputs.
                let r0 = Value::from_str(&caller.to_string()).unwrap();
                let r1 = Value::from_str("100u64").unwrap();

                (private_key, function_name, vec![r0, r1])
            }
            "wallet.aleo" => {
                // Initialize caller 0.
                let caller0_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
                let caller0 = Address::try_from(&caller0_private_key).unwrap();

                // Initialize caller 1.
                let caller1_private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap();
                let caller1 = Address::try_from(&caller1_private_key).unwrap();

                // Declare the function name.
                let function_name = Identifier::from_str("transfer").unwrap();

                // Initialize the function inputs.
                let r0 = Value::<CurrentNetwork>::from_str(&format!(
                    "{{ owner: {caller0}.private, gates: 0u64.private, amount: 100u64.private, _nonce: 0group.public }}"
                ))
                .unwrap();
                let r1 = Value::<CurrentNetwork>::from_str(&caller1.to_string()).unwrap();
                let r2 = Value::<CurrentNetwork>::from_str("99u64").unwrap();

                (caller0_private_key, function_name, vec![r0, r1, r2])
            }
            _ => panic!("Invalid program ID for sample package (while testing)"),
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_imports_directory() {
        // Samples a new package at a temporary directory.
        let (directory, package) = crate::package::test_helpers::sample_package();

        // Ensure the imports directory is correct.
        assert_eq!(package.imports_directory(), directory.join("imports"));
        // Ensure the imports directory does *not* exist, when the package does not contain imports.
        assert!(!package.imports_directory().exists());

        // Proactively remove the temporary directory (to conserve space).
        std::fs::remove_dir_all(directory).unwrap();
    }

    #[test]
    fn test_imports_directory_with_an_import() {
        // Samples a new package with an import at a temporary directory.
        let (directory, package) = crate::package::test_helpers::sample_package_with_import();

        // Ensure the imports directory is correct.
        assert_eq!(package.imports_directory(), directory.join("imports"));
        // Ensure the imports directory exists, as the package contains an import.
        assert!(package.imports_directory().exists());

        // Proactively remove the temporary directory (to conserve space).
        std::fs::remove_dir_all(directory).unwrap();
    }

    #[test]
    fn test_build_directory() {
        // Samples a new package at a temporary directory.
        let (directory, package) = crate::package::test_helpers::sample_package();

        // Ensure the build directory is correct.
        assert_eq!(package.build_directory(), directory.join("build"));
        // Ensure the build directory does *not* exist, when the package has not been built.
        assert!(!package.build_directory().exists());

        // Proactively remove the temporary directory (to conserve space).
        std::fs::remove_dir_all(directory).unwrap();
    }

    #[test]
    fn test_get_process() {
        // Samples a new package at a temporary directory.
        let (directory, package) = crate::package::test_helpers::sample_package();

        // Get the program process and check all instructions.
        assert!(package.get_process().is_ok());

        // Proactively remove the temporary directory (to conserve space).
        std::fs::remove_dir_all(directory).unwrap();
    }
}
