// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 The Gleam contributors

use ecow::EcoString;

use crate::{
    ast::{Publicity, SrcSpan, UnqualifiedImport, UntypedImport},
    build::Origin,
    reference::{EntityKind, ReferenceKind},
    type_::{
        Environment, Error, ModuleInterface, Problems, ValueConstructorVariant, Warning,
        error::InvalidImportKind,
    },
};

use super::Imported;

#[derive(Debug)]
pub struct Importer<'context, 'problems> {
    origin: Origin,
    environment: Environment<'context>,
    problems: &'problems mut Problems,
}

impl<'context, 'problems> Importer<'context, 'problems> {
    pub fn new(
        origin: Origin,
        environment: Environment<'context>,
        problems: &'problems mut Problems,
    ) -> Self {
        Self {
            origin,
            environment,
            problems,
        }
    }

    pub fn run<'code>(
        origin: Origin,
        env: Environment<'context>,
        imports: &'code [UntypedImport],
        problems: &'problems mut Problems,
    ) -> Environment<'context> {
        let mut importer = Self::new(origin, env, problems);
        for import in imports {
            importer.register_import(import)
        }
        importer.environment
    }

    fn register_import(&mut self, import: &UntypedImport) {
        let location = import.location;
        let name = import.module.clone();

        // Find imported module
        let Some(module_info) = self.environment.importable_modules.get(&name) else {
            self.problems.error(Error::UnknownModule {
                location,
                name: name.clone(),
                suggestions: self.environment.suggest_modules(&name, Imported::Module),
            });
            return;
        };

        if let Err(e) = self.check_for_invalid_imports(module_info, location) {
            self.problems.error(e);
        }

        if let Err(e) = self.register_module(import, module_info) {
            self.problems.error(e);
            return;
        }

        // Insert unqualified imports into scope
        let module_name = &module_info.name;
        for type_ in &import.unqualified_types {
            self.register_unqualified_type(type_, module_name.clone(), module_info);
        }
        for value in &import.unqualified_values {
            self.register_unqualified_value(value, module_name.clone(), module_info);
        }
    }

    fn register_unqualified_type(
        &mut self,
        import: &UnqualifiedImport,
        module_name: EcoString,
        module: &ModuleInterface,
    ) {
        let imported_name = import.as_name.as_ref().unwrap_or(&import.name);

        // Register the unqualified import if it is a type constructor
        let Some(type_info) = module.get_importable_type(&import.name) else {
            // TODO: refine to a type specific error
            self.problems.error(Error::UnknownModuleType {
                location: import.location,
                name: import.name.clone(),
                module_name: module.name.clone(),
                type_constructors: module.public_type_names(),
                value_with_same_name: module.get_importable_value(&import.name).is_some(),
            });
            return;
        };

        let type_info = type_info.clone().with_location(import.location);

        self.environment.names.type_in_scope(
            imported_name.clone(),
            type_info.type_.as_ref(),
            &type_info.parameters,
        );

        self.environment.references.register_type(
            imported_name.clone(),
            EntityKind::ImportedType {
                module: module_name,
            },
            import.location,
            Publicity::Private,
        );

        let alias_location = SrcSpan {
            start: import.name_location().end,
            end: import.location.end,
        };

        self.environment.references.register_type_reference(
            type_info.module.clone(),
            import.name.clone(),
            imported_name,
            import.name_location(),
            ReferenceKind::Import(alias_location),
        );

        if let Err(e) = self
            .environment
            .insert_type_constructor(imported_name.clone(), type_info)
        {
            self.problems.error(e);
        }
    }

    fn register_unqualified_value(
        &mut self,
        import: &UnqualifiedImport,
        module_name: EcoString,
        module: &ModuleInterface,
    ) {
        let import_name = &import.name;
        let location = import.location;
        let used_name = import.as_name.as_ref().unwrap_or(&import.name);

        // Register the unqualified import if it is a value
        let variant = match module.get_importable_value(import_name) {
            Some(value) => {
                let implementations = value.variant.implementations();
                // Check the target support of the imported value
                if self.environment.target_support.is_enforced()
                    && !implementations.supports(self.environment.target)
                {
                    self.problems.error(Error::UnsupportedExpressionTarget {
                        target: self.environment.target,
                        location,
                    })
                }

                self.environment.insert_variable(
                    used_name.clone(),
                    value.variant.clone(),
                    value.type_.clone(),
                    value.publicity,
                    value.deprecation.clone(),
                );
                &value.variant
            }
            None => {
                self.problems.error(Error::UnknownModuleValue {
                    location,
                    name: import_name.clone(),
                    module_name: module.name.clone(),
                    value_constructors: module.public_value_names(),
                    type_with_same_name: module.get_importable_type(import_name).is_some(),
                    context: crate::type_::error::ModuleValueUsageContext::UnqualifiedImport,
                });
                return;
            }
        };

        match variant {
            ValueConstructorVariant::Record { name, module, .. } => {
                self.environment.names.named_constructor_in_scope(
                    module.clone(),
                    name.clone(),
                    used_name.clone(),
                );
                self.environment.references.register_value(
                    used_name.clone(),
                    EntityKind::ImportedConstructor {
                        module: module_name,
                    },
                    location,
                    Publicity::Private,
                );

                let alias_location = SrcSpan {
                    start: import.name_location().end,
                    end: import.location.end,
                };

                self.environment.references.register_value_reference(
                    module.clone(),
                    import_name.clone(),
                    used_name,
                    import.name_location(),
                    ReferenceKind::Import(alias_location),
                );
            }
            ValueConstructorVariant::ModuleConstant { module, .. }
            | ValueConstructorVariant::ModuleFn { module, .. } => {
                self.environment.references.register_value(
                    used_name.clone(),
                    EntityKind::ImportedValue {
                        module: module_name,
                    },
                    location,
                    Publicity::Private,
                );

                let alias_location = SrcSpan {
                    start: import.name_location().end,
                    end: import.location.end,
                };

                self.environment.references.register_value_reference(
                    module.clone(),
                    import_name.clone(),
                    used_name,
                    import.name_location(),
                    ReferenceKind::Import(alias_location),
                );
            }
            ValueConstructorVariant::LocalVariable { .. } => {}
        };

        // Check if value already was imported
        if let Some(previous) = self.environment.unqualified_imported_names.get(used_name) {
            self.problems.error(Error::DuplicateImport {
                location,
                previous_location: *previous,
                name: import_name.clone(),
            });
            return;
        }

        // Register the name as imported so it can't be imported a
        // second time in future
        let _ = self
            .environment
            .unqualified_imported_names
            .insert(used_name.clone(), location);
    }

    /// Check for invalid imports, such as `src` importing `test` or `dev`.
    fn check_for_invalid_imports(
        &mut self,
        module_info: &ModuleInterface,
        location: SrcSpan,
    ) -> Result<(), Error> {
        if self.origin.is_src()
            && self
                .environment
                .dev_dependencies
                .contains(&module_info.package)
        {
            return Err(Error::SrcImportingDevDependency {
                importing_module: self.environment.current_module.clone(),
                imported_module: module_info.name.clone(),
                package: module_info.package.clone(),
                location,
            });
        }

        let kind = match (self.origin, module_info.origin) {
            // `src` cannot import `test` or `dev`
            (Origin::Src, Origin::Test) => InvalidImportKind::SrcImportingTest,
            (Origin::Src, Origin::Dev) => InvalidImportKind::SrcImportingDev,
            // `dev` cannot import `test`
            (Origin::Dev, Origin::Test) => InvalidImportKind::DevImportingTest,
            _ => return Ok(()),
        };

        Err(Error::InvalidImport {
            location,
            importing_module: self.environment.current_module.clone(),
            imported_module: module_info.name.clone(),
            kind,
        })
    }

    fn register_module(
        &mut self,
        import: &UntypedImport,
        import_info: &'context ModuleInterface,
    ) -> Result<(), Error> {
        let Some(used_name) = import.used_name() else {
            return Ok(());
        };

        self.check_not_a_duplicate_import(&used_name, import.location)?;

        if let Some(alias_location) = import.alias_location() {
            self.environment.references.register_aliased_module(
                used_name.clone(),
                import.module.clone(),
                alias_location,
                import.location,
                import.module_location,
            );
        } else {
            self.environment.references.register_module(
                used_name.clone(),
                import.module.clone(),
                import.location,
                import.module_location,
                None,
            );
        }

        // Insert imported module into scope
        let _ = self
            .environment
            .imported_modules
            .insert(used_name.clone(), (import.location, import_info));

        // Register this module as being imported
        //
        // Emit a warning if the module had already been imported.
        // This isn't an error so long as the modules have different local aliases. In Gleam v2
        // this will likely become an error.
        if let Some(previous) = self.environment.names.imported_module(
            import.module.clone(),
            used_name,
            import.location,
        ) {
            self.problems.warning(Warning::ModuleImportedTwice {
                name: import.module.clone(),
                first: previous,
                second: import.location,
            });
        }

        Ok(())
    }

    fn check_not_a_duplicate_import(
        &self,
        used_name: &EcoString,
        location: SrcSpan,
    ) -> Result<(), Error> {
        // Check if a module was already imported with this name
        if let Some((previous_location, _)) = self.environment.imported_modules.get(used_name) {
            return Err(Error::DuplicateImport {
                location,
                previous_location: *previous_location,
                name: used_name.clone(),
            });
        }
        Ok(())
    }
}
