//! A configurable artifacts handler implementation
//!
//! Configuring artifacts requires two pieces: the `ConfigurableArtifacts` handler, which contains
//! the configuration of how to construct the `ConfigurableArtifact` type based on a `Contract`. The
//! `ConfigurableArtifacts` populates a single `Artifact`, the `ConfigurableArtifact`, by default
//! with essential entries only, such as `abi`, `bytecode`,..., but may include additional values
//! based on its `ExtraOutputValues` that maps to various objects in the solc contract output, see
//! also: [`OutputSelection`](crate::artifacts::output_selection::OutputSelection). In addition to
//! that some output values can also be emitted as standalone files.

use crate::{
    artifacts::{
        bytecode::{CompactBytecode, CompactDeployedBytecode},
        contract::{CompactContract, CompactContractBytecode, Contract},
        lowfidelity::Ast,
        output_selection::{
            BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection,
            EwasmOutputSelection,
        },
        CompactContractBytecodeCow, DevDoc, Evm, Ewasm, FunctionDebugData, GasEstimates,
        GeneratedSource, LosslessAbi, LosslessMetadata, Metadata, Offsets,
        Settings, /*SourceUnit,*/
        StorageLayout, UserDoc,
    },
    sources::VersionedSourceFile,
    ArtifactOutput, SolcConfig, SolcError, SourceFile,
};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, collections::BTreeMap, fs, path::Path};

/// Represents the `Artifact` that `ConfigurableArtifacts` emits.
///
/// This is essentially a superset of [`CompactContractBytecode`].
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ConfigurableContractArtifact {
    /// The Ethereum Contract ABI. If empty, it is represented as an empty
    /// array. See <https://docs.soliditylang.org/en/develop/abi-spec.html>
    pub abi: Option<LosslessAbi>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bytecode: Option<CompactBytecode>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deployed_bytecode: Option<CompactDeployedBytecode>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub assembly: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub opcodes: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub method_identifiers: Option<BTreeMap<String, String>>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub generated_sources: Vec<GeneratedSource>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub function_debug_data: Option<BTreeMap<String, FunctionDebugData>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub gas_estimates: Option<GasEstimates>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub raw_metadata: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub metadata: Option<Metadata>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub storage_layout: Option<StorageLayout>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub userdoc: Option<UserDoc>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub devdoc: Option<DevDoc>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ir: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ir_optimized: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ewasm: Option<Ewasm>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub ast: Option<Ast>,
    /// The identifier of the source file
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<u32>,
}

impl ConfigurableContractArtifact {
    /// Returns the inner element that contains the core bytecode related information
    pub fn into_contract_bytecode(self) -> CompactContractBytecode {
        self.into()
    }

    /// Looks for all link references in deployment and runtime bytecodes
    pub fn all_link_references(&self) -> BTreeMap<String, BTreeMap<String, Vec<Offsets>>> {
        let mut links = BTreeMap::new();
        if let Some(bcode) = &self.bytecode {
            links.extend(bcode.link_references.clone());
        }

        if let Some(d_bcode) = &self.deployed_bytecode {
            if let Some(bcode) = &d_bcode.bytecode {
                links.extend(bcode.link_references.clone());
            }
        }
        links
    }

    /// Returns the source file of this artifact's contract
    pub fn source_file(&self) -> Option<SourceFile> {
        self.id.map(|id| SourceFile { id, ast: self.ast.clone() })
    }
}

impl From<ConfigurableContractArtifact> for CompactContractBytecode {
    fn from(artifact: ConfigurableContractArtifact) -> Self {
        CompactContractBytecode {
            abi: artifact.abi.map(Into::into),
            bytecode: artifact.bytecode,
            deployed_bytecode: artifact.deployed_bytecode,
        }
    }
}

impl From<ConfigurableContractArtifact> for CompactContract {
    fn from(artifact: ConfigurableContractArtifact) -> Self {
        CompactContractBytecode::from(artifact).into()
    }
}

impl<'a> From<&'a ConfigurableContractArtifact> for CompactContractBytecodeCow<'a> {
    fn from(artifact: &'a ConfigurableContractArtifact) -> Self {
        CompactContractBytecodeCow {
            abi: artifact.abi.as_ref().map(|abi| Cow::Borrowed(&abi.abi)),
            bytecode: artifact.bytecode.as_ref().map(Cow::Borrowed),
            deployed_bytecode: artifact.deployed_bytecode.as_ref().map(Cow::Borrowed),
        }
    }
}

/// An `Artifact` implementation that can be configured to include additional content and emit
/// additional files
///
/// Creates a single json artifact with
/// ```json
///  {
///    "abi": [],
///    "bytecode": {...},
///    "deployedBytecode": {...},
///    "methodIdentifiers": {...},
///    // additional values
///  }
/// ```
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub struct ConfigurableArtifacts {
    /// A set of additional values to include in the contract's artifact file
    pub additional_values: ExtraOutputValues,

    /// A set of values that should be written to a separate file
    pub additional_files: ExtraOutputFiles,

    /// PRIVATE: This structure may grow, As such, constructing this structure should
    /// _always_ be done using a public constructor or update syntax:
    ///
    /// ```rust
    ///
    /// use ethers_solc::{ExtraOutputFiles, ConfigurableArtifacts};
    /// let config = ConfigurableArtifacts {
    ///     additional_files: ExtraOutputFiles { metadata: true, ..Default::default() },
    ///     ..Default::default()
    /// };
    /// ```
    #[doc(hidden)]
    pub __non_exhaustive: (),
}

impl ConfigurableArtifacts {
    pub fn new(
        extra_values: impl IntoIterator<Item = ContractOutputSelection>,
        extra_files: impl IntoIterator<Item = ContractOutputSelection>,
    ) -> Self {
        Self {
            additional_values: ExtraOutputValues::from_output_selection(extra_values),
            additional_files: ExtraOutputFiles::from_output_selection(extra_files),
            ..Default::default()
        }
    }

    /// Returns the `Settings` this configuration corresponds to
    pub fn settings(&self) -> Settings {
        SolcConfig::builder().additional_outputs(self.output_selection()).build().into()
    }

    /// Returns the output selection corresponding to this configuration
    pub fn output_selection(&self) -> Vec<ContractOutputSelection> {
        let mut selection = ContractOutputSelection::basic();

        if self.additional_values.ir {
            selection.push(ContractOutputSelection::Ir);
        }
        if self.additional_values.ir_optimized || self.additional_files.ir_optimized {
            selection.push(ContractOutputSelection::IrOptimized);
        }
        if self.additional_values.metadata || self.additional_files.metadata {
            selection.push(ContractOutputSelection::Metadata);
        }
        if self.additional_values.storage_layout {
            selection.push(ContractOutputSelection::StorageLayout);
        }
        if self.additional_values.devdoc {
            selection.push(ContractOutputSelection::DevDoc);
        }
        if self.additional_values.userdoc {
            selection.push(ContractOutputSelection::UserDoc);
        }
        if self.additional_values.gas_estimates {
            selection.push(EvmOutputSelection::GasEstimates.into());
        }
        if self.additional_values.assembly || self.additional_files.assembly {
            selection.push(EvmOutputSelection::Assembly.into());
        }
        if self.additional_values.ewasm || self.additional_files.ewasm {
            selection.push(EwasmOutputSelection::All.into());
        }
        if self.additional_values.function_debug_data {
            selection.push(BytecodeOutputSelection::FunctionDebugData.into());
        }
        if self.additional_values.method_identifiers {
            selection.push(EvmOutputSelection::MethodIdentifiers.into());
        }
        if self.additional_values.generated_sources {
            selection.push(
                EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources).into(),
            );
        }
        if self.additional_values.source_map {
            selection.push(EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap).into());
        }
        selection
    }
}

impl ArtifactOutput for ConfigurableArtifacts {
    type Artifact = ConfigurableContractArtifact;

    fn write_contract_extras(&self, contract: &Contract, file: &Path) -> Result<(), SolcError> {
        self.additional_files.write_extras(contract, file)
    }

    fn contract_to_artifact(
        &self,
        _file: &str,
        _name: &str,
        contract: Contract,
        source_file: Option<&SourceFile>,
    ) -> Self::Artifact {
        let mut artifact_userdoc = None;
        let mut artifact_devdoc = None;
        let mut artifact_raw_metadata = None;
        let mut artifact_metadata = None;
        let mut artifact_ir = None;
        let mut artifact_ir_optimized = None;
        let mut artifact_ewasm = None;
        let mut artifact_bytecode = None;
        let mut artifact_deployed_bytecode = None;
        let mut artifact_gas_estimates = None;
        let mut artifact_function_debug_data = None;
        let mut artifact_method_identifiers = None;
        let mut artifact_assembly = None;
        let mut artifact_storage_layout = None;
        let mut generated_sources = None;
        let mut opcodes = None;

        let Contract {
            abi,
            metadata,
            userdoc,
            devdoc,
            ir,
            storage_layout,
            evm,
            ewasm,
            ir_optimized,
        } = contract;

        if self.additional_values.metadata {
            if let Some(LosslessMetadata { raw_metadata, metadata }) = metadata {
                artifact_raw_metadata = Some(raw_metadata);
                artifact_metadata = Some(metadata);
            }
        }
        if self.additional_values.userdoc {
            artifact_userdoc = Some(userdoc);
        }
        if self.additional_values.devdoc {
            artifact_devdoc = Some(devdoc);
        }
        if self.additional_values.ewasm {
            artifact_ewasm = ewasm;
        }
        if self.additional_values.ir {
            artifact_ir = ir;
        }
        if self.additional_values.ir_optimized {
            artifact_ir_optimized = ir_optimized;
        }
        if self.additional_values.storage_layout {
            artifact_storage_layout = Some(storage_layout);
        }

        if let Some(evm) = evm {
            let Evm {
                assembly,
                mut bytecode,
                deployed_bytecode,
                method_identifiers,
                gas_estimates,
                ..
            } = evm;

            if self.additional_values.function_debug_data {
                artifact_function_debug_data =
                    bytecode.as_mut().map(|code| std::mem::take(&mut code.function_debug_data));
            }
            if self.additional_values.generated_sources {
                generated_sources =
                    bytecode.as_mut().map(|code| std::mem::take(&mut code.generated_sources));
            }

            if self.additional_values.opcodes {
                opcodes = bytecode.as_mut().and_then(|code| code.opcodes.take())
            }

            artifact_bytecode = bytecode.map(Into::into);
            artifact_deployed_bytecode = deployed_bytecode.map(Into::into);
            artifact_method_identifiers = Some(method_identifiers);

            if self.additional_values.gas_estimates {
                artifact_gas_estimates = gas_estimates;
            }
            if self.additional_values.assembly {
                artifact_assembly = assembly;
            }
        }

        ConfigurableContractArtifact {
            abi,
            bytecode: artifact_bytecode,
            deployed_bytecode: artifact_deployed_bytecode,
            assembly: artifact_assembly,
            opcodes,
            function_debug_data: artifact_function_debug_data,
            method_identifiers: artifact_method_identifiers,
            gas_estimates: artifact_gas_estimates,
            raw_metadata: artifact_raw_metadata,
            metadata: artifact_metadata,
            storage_layout: artifact_storage_layout,
            userdoc: artifact_userdoc,
            devdoc: artifact_devdoc,
            ir: artifact_ir,
            ir_optimized: artifact_ir_optimized,
            ewasm: artifact_ewasm,
            id: source_file.as_ref().map(|s| s.id),
            ast: source_file.and_then(|s| s.ast.clone()),
            generated_sources: generated_sources.unwrap_or_default(),
        }
    }

    fn standalone_source_file_to_artifact(
        &self,
        _path: &str,
        file: &VersionedSourceFile,
    ) -> Option<Self::Artifact> {
        file.source_file.ast.clone().map(|ast| ConfigurableContractArtifact {
            abi: Some(LosslessAbi::default()),
            id: Some(file.source_file.id),
            ast: Some(ast),
            bytecode: Some(CompactBytecode::empty()),
            deployed_bytecode: Some(CompactDeployedBytecode::empty()),
            ..Default::default()
        })
    }
}

/// Determines the additional values to include in the contract's artifact file
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub struct ExtraOutputValues {
    pub ast: bool,
    pub userdoc: bool,
    pub devdoc: bool,
    pub method_identifiers: bool,
    pub storage_layout: bool,
    pub assembly: bool,
    pub gas_estimates: bool,
    pub compact_format: bool,
    pub metadata: bool,
    pub ir: bool,
    pub ir_optimized: bool,
    pub ewasm: bool,
    pub function_debug_data: bool,
    pub generated_sources: bool,
    pub source_map: bool,
    pub opcodes: bool,

    /// PRIVATE: This structure may grow, As such, constructing this structure should
    /// _always_ be done using a public constructor or update syntax:
    ///
    /// ```rust
    ///
    /// use ethers_solc::ExtraOutputValues;
    /// let config = ExtraOutputValues {
    ///     ir: true,
    ///     ..Default::default()
    /// };
    /// ```
    #[doc(hidden)]
    pub __non_exhaustive: (),
}

impl ExtraOutputValues {
    /// Returns an instance where all values are set to `true`
    pub fn all() -> Self {
        Self {
            ast: true,
            userdoc: true,
            devdoc: true,
            method_identifiers: true,
            storage_layout: true,
            assembly: true,
            gas_estimates: true,
            compact_format: true,
            metadata: true,
            ir: true,
            ir_optimized: true,
            ewasm: true,
            function_debug_data: true,
            generated_sources: true,
            source_map: true,
            opcodes: true,
            __non_exhaustive: (),
        }
    }

    /// Sets the values based on a set of `ContractOutputSelection`
    pub fn from_output_selection(
        settings: impl IntoIterator<Item = ContractOutputSelection>,
    ) -> Self {
        let mut config = Self::default();
        for value in settings.into_iter() {
            match value {
                ContractOutputSelection::DevDoc => {
                    config.devdoc = true;
                }
                ContractOutputSelection::UserDoc => {
                    config.userdoc = true;
                }
                ContractOutputSelection::Metadata => {
                    config.metadata = true;
                }
                ContractOutputSelection::Ir => {
                    config.ir = true;
                }
                ContractOutputSelection::IrOptimized => {
                    config.ir_optimized = true;
                }
                ContractOutputSelection::StorageLayout => {
                    config.storage_layout = true;
                }
                ContractOutputSelection::Evm(evm) => match evm {
                    EvmOutputSelection::All => {
                        config.assembly = true;
                        config.gas_estimates = true;
                        config.method_identifiers = true;
                        config.generated_sources = true;
                        config.source_map = true;
                        config.opcodes = true;
                    }
                    EvmOutputSelection::Assembly => {
                        config.assembly = true;
                    }
                    EvmOutputSelection::MethodIdentifiers => {
                        config.method_identifiers = true;
                    }
                    EvmOutputSelection::GasEstimates => {
                        config.gas_estimates = true;
                    }
                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::FunctionDebugData) => {
                        config.function_debug_data = true;
                    }
                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::Opcodes) => {
                        config.opcodes = true;
                    }
                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources) => {
                        config.generated_sources = true;
                    }
                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap) => {
                        config.source_map = true;
                    }
                    _ => {}
                },
                ContractOutputSelection::Ewasm(_) => {
                    config.ewasm = true;
                }
                _ => {}
            }
        }

        config
    }
}

/// Determines what to emit as additional file
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub struct ExtraOutputFiles {
    pub abi: bool,
    pub metadata: bool,
    pub ir_optimized: bool,
    pub ewasm: bool,
    pub assembly: bool,
    pub source_map: bool,
    pub generated_sources: bool,

    /// PRIVATE: This structure may grow, As such, constructing this structure should
    /// _always_ be done using a public constructor or update syntax:
    ///
    /// ```rust
    ///
    /// use ethers_solc::ExtraOutputFiles;
    /// let config = ExtraOutputFiles {
    ///     metadata: true,
    ///     ..Default::default()
    /// };
    /// ```
    #[doc(hidden)]
    pub __non_exhaustive: (),
}

impl ExtraOutputFiles {
    /// Returns an instance where all values are set to `true`
    pub fn all() -> Self {
        Self {
            abi: true,
            metadata: true,
            ir_optimized: true,
            ewasm: true,
            assembly: true,
            source_map: true,
            generated_sources: true,
            __non_exhaustive: (),
        }
    }

    /// Sets the values based on a set of `ContractOutputSelection`
    pub fn from_output_selection(
        settings: impl IntoIterator<Item = ContractOutputSelection>,
    ) -> Self {
        let mut config = Self::default();
        for value in settings.into_iter() {
            match value {
                ContractOutputSelection::Abi => {
                    config.abi = true;
                }
                ContractOutputSelection::Metadata => {
                    config.metadata = true;
                }
                ContractOutputSelection::IrOptimized => {
                    config.ir_optimized = true;
                }
                ContractOutputSelection::Evm(evm) => match evm {
                    EvmOutputSelection::All => {
                        config.assembly = true;
                        config.generated_sources = true;
                        config.source_map = true;
                    }
                    EvmOutputSelection::Assembly => {
                        config.assembly = true;
                    }
                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources) => {
                        config.generated_sources = true;
                    }
                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap) => {
                        config.source_map = true;
                    }
                    _ => {}
                },
                ContractOutputSelection::Ewasm(_) => {
                    config.ewasm = true;
                }
                _ => {}
            }
        }
        config
    }

    /// Write the set values as separate files
    pub fn write_extras(&self, contract: &Contract, file: &Path) -> Result<(), SolcError> {
        if self.abi {
            if let Some(ref abi) = contract.abi {
                let file = file.with_extension("abi.json");
                fs::write(&file, serde_json::to_string_pretty(abi)?)
                    .map_err(|err| SolcError::io(err, file))?
            }
        }

        if self.metadata {
            if let Some(ref metadata) = contract.metadata {
                let file = file.with_extension("metadata.json");
                fs::write(&file, serde_json::to_string_pretty(&metadata.raw_json()?)?)
                    .map_err(|err| SolcError::io(err, file))?
            }
        }

        if self.ir_optimized {
            if let Some(ref iropt) = contract.ir_optimized {
                let file = file.with_extension("iropt");
                fs::write(&file, iropt).map_err(|err| SolcError::io(err, file))?
            }
        }

        if self.ewasm {
            if let Some(ref ir) = contract.ir {
                let file = file.with_extension("ir");
                fs::write(&file, ir).map_err(|err| SolcError::io(err, file))?
            }
        }

        if self.ewasm {
            if let Some(ref ewasm) = contract.ewasm {
                let file = file.with_extension("ewasm");
                fs::write(&file, serde_json::to_vec_pretty(ewasm)?)
                    .map_err(|err| SolcError::io(err, file))?;
            }
        }

        if self.assembly {
            if let Some(ref evm) = contract.evm {
                if let Some(ref asm) = evm.assembly {
                    let file = file.with_extension("asm");
                    fs::write(&file, asm).map_err(|err| SolcError::io(err, file))?
                }
            }
        }

        if self.generated_sources {
            if let Some(ref evm) = contract.evm {
                if let Some(ref bytecode) = evm.bytecode {
                    let file = file.with_extension("gensources");
                    fs::write(&file, serde_json::to_vec_pretty(&bytecode.generated_sources)?)
                        .map_err(|err| SolcError::io(err, file))?;
                }
            }
        }

        if self.source_map {
            if let Some(ref evm) = contract.evm {
                if let Some(ref bytecode) = evm.bytecode {
                    if let Some(ref sourcemap) = bytecode.source_map {
                        let file = file.with_extension("sourcemap");
                        fs::write(&file, sourcemap).map_err(|err| SolcError::io(err, file))?
                    }
                }
            }
        }

        Ok(())
    }
}