use std::{
    fmt::Display,
    fs::File,
    path::{Path, PathBuf},
};

use crate::common::{Binding, Relocatable, Section, SectionInfo, Symbol, SymbolIterBox};
use crate::{common::SectionIterBox, error::Error};
use elf_utilities::{
    header::Ehdr64,
    section::{Contents64, Shdr64, Type},
    symbol::{Bind, Symbol64},
};
use memmap::Mmap;

pub struct ElfObject {
    object_index: usize,
    origin: PathBuf,
    data: Mmap,
    ehdr: Ehdr64,
    shdrs: Vec<Shdr64>,
    symbols: Vec<Symbol64>,
}

impl ElfObject {
    pub fn new(origin: PathBuf, object_index: usize) -> Result<Self, Error> {
        let str_origin = origin
            .as_path()
            .to_str()
            .ok_or(Error::InvalidObjectType(404))?;

        let file = File::open(str_origin)?;
        let data = unsafe { Mmap::map(&file)? };
        let ehdr = parse_elf_header(&data)?;

        let mut result = ElfObject {
            object_index,
            origin,
            data,
            ehdr,
            shdrs: Vec::new(),
            symbols: Vec::new(),
        };

        // TODO: make paralellization from outside possible?
        result.populate()?;

        Ok(result)
    }

    fn resolve_name(&self, name_idx: usize, sh_index: usize) -> Result<&str, Error> {
        let strtab = self.parse_strtab(sh_index)?;
        let strtab_offset: usize = strtab.sh_offset as usize;
        let start = strtab_offset + name_idx;

        let mut i = start;
        while self.data[i] != 0 {
            i += 1;
            // sanity check
            if i - start > 1024 {
                return Err(Error::DataError(self.origin.clone()));
            }
        }

        // TODO: consider unchecked?
        std::str::from_utf8(&self.data[start..i]).map_err(|err| Error::ParseError(err.into()))
    }

    // TODO: consider caching these
    fn parse_strtab(&self, sh_index: usize) -> Result<Shdr64, Error> {
        let sh_start: usize = self.ehdr.e_shoff as usize;
        let sh_size: usize = self.ehdr.e_shentsize.into();
        let offset: usize = sh_start + sh_size * sh_index;

        bincode::deserialize(&self.data[offset..offset + sh_size])
            .map_err(|err| Error::ParseError(err))
    }

    fn populate(&mut self) -> Result<(), Error> {
        for i in 0..self.ehdr.e_shnum {
            let i_usize: usize = i.into();
            let sh_start = self.ehdr.e_shoff as usize;
            let sh_size = usize::from(self.ehdr.e_shentsize);

            let offset: usize = sh_start + i_usize * sh_size;
            let shdr: Shdr64 = bincode::deserialize(&self.data[offset..offset + sh_size])
                .map_err(|err| Error::ParseError(err))?;

            if shdr.get_type() == Type::SymTab {
                let ent_size = shdr.sh_entsize as usize;
                let offset = shdr.sh_offset as usize;
                let count = shdr.sh_size as usize / ent_size;

                for s_i in 0..count {
                    let start = offset + s_i * ent_size;
                    let s64 = Symbol64::deserialize(&self.data, start)
                        .map_err(|err| Error::ParseError(err))?;
                    self.symbols.push(s64);
                }
            }
            // self.symbols.push();
            self.shdrs.push(shdr);
        }

        Ok(())
    }

    fn make_section(&self, shdr: &Shdr64) -> Option<Result<Section, Error>> {
        let sh_index: usize = self.ehdr.e_shstrndx.into();
        let name = match self.resolve_name(shdr.sh_name as usize, sh_index) {
            Ok(n) => n,
            Err(err) => return Some(Err(err)),
        };

        let mut si = SectionInfo {
            object_index: self.object_index,
            file_size: shdr.sh_size,
            data_size: shdr.sh_size,
            offset: shdr.sh_offset,
        };

        if name.starts_with(".bss") {
            si.file_size = 0;
            Some(Ok(Section::Bss(si)))
        } else if name.starts_with(".text") {
            Some(Ok(Section::Text(si)))
        } else if name.starts_with(".rodata") {
            Some(Ok(Section::Data(si, true)))
        } else if name.starts_with(".data") {
            Some(Ok(Section::Data(si, false)))
        } else {
            None
        }
    }

    fn make_symbol(&self, s64: &Symbol64) -> Symbol {
        let binding = match s64.get_bind() {
            Bind::Global => Binding::Global,
            Bind::Local => Binding::Local,
            Bind::Weak => Binding::Weak,
            _ => panic!("Unexpected binding type encountered on symbol"), // this is screened!
        };

        Symbol {
            object_index: self.object_index,
            binding,
            address: s64.st_value,
            size: s64.st_size,
        }
    }
}

impl Relocatable for ElfObject {
    fn new(origin: PathBuf, object_index: usize) -> Result<Self, Error> {
        ElfObject::new(origin, object_index)
    }

    fn origin(&self) -> &Path {
        &self.origin
    }

    fn sections(&self) -> SectionIterBox {
        let iter = self
            .shdrs
            .iter()
            .filter_map(move |shdr| match shdr.get_type() {
                Type::ProgBits | Type::NoBits => self.make_section(shdr),
                _ => None,
            });

        Box::new(iter)
    }

    fn bytes(&self, offset: u64, size: u64) -> Result<&[u8], Error> {
        let o = offset as usize;
        let s = size as usize;
        Ok(&self.data[o..o + s])
    }

    fn symbols(&self) -> SymbolIterBox {
        Box::new(self.symbols.iter().map(move |s| {
            // TODO: change SymbolIterBox to allow errors!
            let name = self
                .resolve_name(s.st_name as usize, s.st_other as usize)
                .expect("Symbol name resolution");
            (name, self.make_symbol(s))
        }))
    }
}

impl Display for ElfObject {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "==={:?}===\n>Symbols:\n{}\n>Sections:\n",
            self.origin().file_name().unwrap(),
            "TODO"
        )?;

        for section in self.sections() {
            let u = section.unwrap();
            write!(f, "{}", u)?;
        }

        Ok(())
    }
}

fn parse_strtab_name(strtab: &[u8], idx: u32) -> String {
    let bytes: Vec<u8> = strtab
        .iter()
        .skip(idx as usize)
        .take_while(|byte| **byte != 0x00)
        .copied()
        .collect();

    std::str::from_utf8(&bytes)
        .expect("Symbol name parse")
        .to_string()
}

fn parse_elf_header(data: &Mmap) -> Result<Ehdr64, Error> {
    use elf_utilities::header::Type;

    let magic = &data[0..4];
    if magic != &[0x7f, 0x45, 0x4c, 0x46] {
        return Err(Error::InvalidInput);
    }

    let ehdr = Ehdr64::deserialize(data, 0).map_err(|err| Error::ParseError(err))?;
    if ehdr.get_type() != Type::Rel {
        return Err(Error::InvalidObjectType(u32::from(ehdr.e_type)));
    }

    Ok(ehdr)
}