/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::io::{BufWriter, Read, Write};
use std::path;
use std::rc::Rc;

use crate::bindgen::config::{Config, Language};
use crate::bindgen::ir::{
    Constant, Function, ItemContainer, ItemMap, Path as BindgenPath, Static, Struct, Type, Typedef,
};
use crate::bindgen::language_backend::{
    CLikeLanguageBackend, CythonLanguageBackend, LanguageBackend,
};
use crate::bindgen::writer::SourceWriter;

/// A bindings header that can be written.
pub struct Bindings {
    pub config: Config,
    /// The map from path to struct, used to lookup whether a given type is a
    /// transparent struct. This is needed to generate code for constants.
    struct_map: ItemMap<Struct>,
    typedef_map: ItemMap<Typedef>,
    struct_fileds_memo: RefCell<HashMap<BindgenPath, Rc<Vec<String>>>>,
    pub globals: Vec<Static>,
    pub constants: Vec<Constant>,
    pub items: Vec<ItemContainer>,
    pub functions: Vec<Function>,
    source_files: Vec<path::PathBuf>,
    /// Bindings are generated by a recursive call to cbindgen
    /// and shouldn't do anything when written anywhere.
    noop: bool,
    pub package_version: String,
}

impl Bindings {
    #[allow(clippy::too_many_arguments)]
    pub(crate) fn new(
        config: Config,
        struct_map: ItemMap<Struct>,
        typedef_map: ItemMap<Typedef>,
        constants: Vec<Constant>,
        globals: Vec<Static>,
        items: Vec<ItemContainer>,
        functions: Vec<Function>,
        source_files: Vec<path::PathBuf>,
        noop: bool,
        package_version: String,
    ) -> Bindings {
        Bindings {
            config,
            struct_map,
            typedef_map,
            struct_fileds_memo: Default::default(),
            globals,
            constants,
            items,
            functions,
            source_files,
            noop,
            package_version,
        }
    }

    // FIXME(emilio): What to do when the configuration doesn't match?
    pub fn struct_is_transparent(&self, path: &BindgenPath) -> bool {
        let mut any = false;
        self.struct_map.for_items(path, |s| any |= s.is_transparent);
        any
    }

    /// Peels through typedefs to allow resolving structs.
    fn resolved_struct_path<'a>(&self, path: &'a BindgenPath) -> Cow<'a, BindgenPath> {
        let mut resolved_path = Cow::Borrowed(path);
        loop {
            let mut found = None;
            self.typedef_map.for_items(&resolved_path, |item| {
                if let Type::Path(ref p) = item.aliased {
                    found = Some(p.path().clone());
                }
            });
            resolved_path = match found {
                Some(p) => Cow::Owned(p),
                None => break,
            }
        }
        resolved_path
    }

    pub fn struct_exists(&self, path: &BindgenPath) -> bool {
        let mut any = false;
        self.struct_map
            .for_items(&self.resolved_struct_path(path), |_| any = true);
        any
    }

    pub fn struct_field_names(&self, path: &BindgenPath) -> Rc<Vec<String>> {
        let mut memos = self.struct_fileds_memo.borrow_mut();
        if let Some(memo) = memos.get(path) {
            return memo.clone();
        }

        let resolved_path = self.resolved_struct_path(path);

        let mut fields = Vec::<String>::new();
        self.struct_map.for_items(&resolved_path, |st| {
            let mut pos: usize = 0;
            for field in &st.fields {
                if let Some(found_pos) = fields.iter().position(|v| *v == field.name) {
                    pos = found_pos + 1;
                } else {
                    fields.insert(pos, field.name.clone());
                    pos += 1;
                }
            }
        });

        let fields = Rc::new(fields);
        memos.insert(path.clone(), fields.clone());
        if let Cow::Owned(p) = resolved_path {
            memos.insert(p, fields.clone());
        }
        fields
    }

    /// Lists the exported symbols that can be dynamically linked, i.e. globals and functions.
    pub fn dynamic_symbols_names(&self) -> impl Iterator<Item = &str> {
        use crate::bindgen::ir::Item;

        let function_names = self
            .functions
            .iter()
            .filter(|f| f.annotations.should_export())
            .map(|f| f.path().name());
        let global_names = self
            .globals
            .iter()
            .filter(|s| s.annotations.should_export())
            .map(|g| g.export_name());
        function_names.chain(global_names)
    }

    pub fn generate_symfile<P: AsRef<path::Path>>(&self, symfile_path: P) {
        if let Some(dir) = symfile_path.as_ref().parent() {
            std::fs::create_dir_all(dir).unwrap();
        }
        let mut writer = BufWriter::new(File::create(symfile_path).unwrap());
        writeln!(&mut writer, "{{").expect("writing symbol file header failed");
        for symbol in self.dynamic_symbols_names() {
            writeln!(&mut writer, "{symbol};").expect("writing symbol failed");
        }
        write!(&mut writer, "}};").expect("writing symbol file footer failed");
    }

    pub fn generate_depfile<P: AsRef<path::Path>>(&self, header_path: P, depfile_path: P) {
        if let Some(dir) = depfile_path.as_ref().parent() {
            if !dir.exists() {
                std::fs::create_dir_all(dir).unwrap()
            }
        }
        let canon_header_path = header_path.as_ref().canonicalize().unwrap();
        let mut canon_source_files: Vec<_> = self
            .source_files
            .iter()
            .chain(self.config.config_path.as_ref())
            .map(|p| p.canonicalize().unwrap())
            .collect();
        // Sorting makes testing easier by ensuring the output is ordered.
        canon_source_files.sort_unstable();

        // When writing the depfile we must escape whitespace in paths to avoid it being interpreted
        // as a seperator.
        // It is not clear how to otherwise _correctly_ replace whitespace in a non-unicode
        // compliant slice, without knowing the encoding, so we lossy convert such cases,
        // to avoid panics.
        let mut depfile = File::create(depfile_path).unwrap();
        write!(
            &mut depfile,
            "{}:",
            canon_header_path.to_string_lossy().replace(' ', "\\ ")
        )
        .expect("Writing header name to depfile failed");
        canon_source_files.into_iter().for_each(|source_file| {
            // Add line-continue and line-break and then indent with 4 spaces.
            // This makes the output more human-readable.
            depfile.write_all(b" \\\n    ").unwrap();
            let escaped_path = source_file.to_string_lossy().replace(' ', "\\ ");
            depfile.write_all(escaped_path.as_bytes()).unwrap();
        });

        writeln!(&mut depfile).unwrap();

        depfile.flush().unwrap();
    }

    pub fn write_to_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
        if self.noop {
            return false;
        }

        // Don't compare files if we've never written this file before
        if !path.as_ref().is_file() {
            if let Some(parent) = path::Path::new(path.as_ref()).parent() {
                fs::create_dir_all(parent).unwrap();
            }
            self.write(File::create(path).unwrap());
            return true;
        }

        let mut new_file_contents = Vec::new();
        self.write(&mut new_file_contents);

        let mut old_file_contents = Vec::new();
        {
            let mut old_file = File::open(&path).unwrap();
            old_file.read_to_end(&mut old_file_contents).unwrap();
        }

        if old_file_contents != new_file_contents {
            let mut new_file = File::create(&path).unwrap();
            new_file.write_all(&new_file_contents).unwrap();
            true
        } else {
            false
        }
    }

    pub fn write<F: Write>(&self, file: F) {
        match self.config.language {
            Language::Cxx | Language::C => {
                self.write_with_backend(file, &mut CLikeLanguageBackend::new(&self.config))
            }
            Language::Cython => {
                self.write_with_backend(file, &mut CythonLanguageBackend::new(&self.config))
            }
        }
    }

    fn write_with_backend<F: Write, LB: LanguageBackend>(
        &self,
        file: F,
        language_backend: &mut LB,
    ) {
        if self.noop {
            return;
        }

        let mut out = SourceWriter::new(file, self);

        language_backend.write_bindings(&mut out, self);
    }
}
