use scroll::ctx::{self, SizeWith};
use scroll::{Pread, Pwrite};

use log::{debug, warn};

use alloc::boxed::Box;
use alloc::vec::Vec;
use core::fmt;
use core::ops::{Deref, DerefMut};

use crate::container;
use crate::error;

use crate::mach::constants::{SECTION_TYPE, S_GB_ZEROFILL, S_THREAD_LOCAL_ZEROFILL, S_ZEROFILL};
use crate::mach::load_command::{
    Section32, Section64, SegmentCommand32, SegmentCommand64, LC_SEGMENT, LC_SEGMENT_64,
    SIZEOF_SECTION_32, SIZEOF_SECTION_64, SIZEOF_SEGMENT_COMMAND_32, SIZEOF_SEGMENT_COMMAND_64,
};
use crate::mach::relocation::RelocationInfo;

pub struct RelocationIterator<'a> {
    data: &'a [u8],
    nrelocs: usize,
    offset: usize,
    count: usize,
    ctx: scroll::Endian,
}

impl<'a> Iterator for RelocationIterator<'a> {
    type Item = error::Result<RelocationInfo>;
    fn next(&mut self) -> Option<Self::Item> {
        if self.count >= self.nrelocs {
            None
        } else {
            self.count += 1;
            match self.data.gread_with(&mut self.offset, self.ctx) {
                Ok(res) => Some(Ok(res)),
                Err(e) => Some(Err(e.into())),
            }
        }
    }
}

/// Generalized 32/64 bit Section
#[derive(Default)]
pub struct Section {
    /// name of this section
    pub sectname: [u8; 16],
    /// segment this section goes in
    pub segname: [u8; 16],
    /// memory address of this section
    pub addr: u64,
    /// size in bytes of this section
    pub size: u64,
    /// file offset of this section
    pub offset: u32,
    /// section alignment (power of 2)
    pub align: u32,
    /// file offset of relocation entries
    pub reloff: u32,
    /// number of relocation entries
    pub nreloc: u32,
    /// flags (section type and attributes
    pub flags: u32,
}

impl Section {
    /// The name of this section
    pub fn name(&self) -> error::Result<&str> {
        Ok(self.sectname.pread::<&str>(0)?)
    }
    /// The containing segment's name
    pub fn segname(&self) -> error::Result<&str> {
        Ok(self.segname.pread::<&str>(0)?)
    }
    /// Iterate this sections relocations given `data`; `data` must be the original binary
    pub fn iter_relocations<'b>(
        &self,
        data: &'b [u8],
        ctx: container::Ctx,
    ) -> RelocationIterator<'b> {
        let offset = self.reloff as usize;
        debug!(
            "Relocations for {} starting at offset: {:#x}",
            self.name().unwrap_or("BAD_SECTION_NAME"),
            offset
        );
        RelocationIterator {
            offset,
            nrelocs: self.nreloc as usize,
            count: 0,
            data,
            ctx: ctx.le,
        }
    }
}

impl From<Section> for Section64 {
    fn from(section: Section) -> Self {
        Section64 {
            sectname: section.sectname,
            segname: section.segname,
            addr: section.addr as u64,
            size: section.size as u64,
            offset: section.offset,
            align: section.align,
            reloff: section.reloff,
            nreloc: section.nreloc,
            flags: section.flags,
            reserved1: 0,
            reserved2: 0,
            reserved3: 0,
        }
    }
}

impl From<Section> for Section32 {
    fn from(section: Section) -> Self {
        Section32 {
            sectname: section.sectname,
            segname: section.segname,
            addr: section.addr as u32,
            size: section.size as u32,
            offset: section.offset,
            align: section.align,
            reloff: section.reloff,
            nreloc: section.nreloc,
            flags: section.flags,
            reserved1: 0,
            reserved2: 0,
        }
    }
}

impl fmt::Debug for Section {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.debug_struct("Section")
            .field("sectname", &self.name().unwrap())
            .field("segname", &self.segname().unwrap())
            .field("addr", &self.addr)
            .field("size", &self.size)
            .field("offset", &self.offset)
            .field("align", &self.align)
            .field("reloff", &self.reloff)
            .field("nreloc", &self.nreloc)
            .field("flags", &self.flags)
            .finish()
    }
}

impl From<Section32> for Section {
    fn from(section: Section32) -> Self {
        Section {
            sectname: section.sectname,
            segname: section.segname,
            addr: u64::from(section.addr),
            size: u64::from(section.size),
            offset: section.offset,
            align: section.align,
            reloff: section.reloff,
            nreloc: section.nreloc,
            flags: section.flags,
        }
    }
}

impl From<Section64> for Section {
    fn from(section: Section64) -> Self {
        Section {
            sectname: section.sectname,
            segname: section.segname,
            addr: section.addr,
            size: section.size,
            offset: section.offset,
            align: section.align,
            reloff: section.reloff,
            nreloc: section.nreloc,
            flags: section.flags,
        }
    }
}

impl<'a> ctx::TryFromCtx<'a, container::Ctx> for Section {
    type Error = crate::error::Error;
    fn try_from_ctx(bytes: &'a [u8], ctx: container::Ctx) -> Result<(Self, usize), Self::Error> {
        match ctx.container {
            container::Container::Little => {
                let section = Section::from(bytes.pread_with::<Section32>(0, ctx.le)?);
                Ok((section, SIZEOF_SECTION_32))
            }
            container::Container::Big => {
                let section = Section::from(bytes.pread_with::<Section64>(0, ctx.le)?);
                Ok((section, SIZEOF_SECTION_64))
            }
        }
    }
}

impl ctx::SizeWith<container::Ctx> for Section {
    fn size_with(ctx: &container::Ctx) -> usize {
        match ctx.container {
            container::Container::Little => SIZEOF_SECTION_32,
            container::Container::Big => SIZEOF_SECTION_64,
        }
    }
}

impl ctx::TryIntoCtx<container::Ctx> for Section {
    type Error = crate::error::Error;
    fn try_into_ctx(self, bytes: &mut [u8], ctx: container::Ctx) -> Result<usize, Self::Error> {
        if ctx.is_big() {
            bytes.pwrite_with::<Section64>(self.into(), 0, ctx.le)?;
        } else {
            bytes.pwrite_with::<Section32>(self.into(), 0, ctx.le)?;
        }
        Ok(Self::size_with(&ctx))
    }
}

impl ctx::IntoCtx<container::Ctx> for Section {
    fn into_ctx(self, bytes: &mut [u8], ctx: container::Ctx) {
        bytes.pwrite_with(self, 0, ctx).unwrap();
    }
}

pub struct SectionIterator<'a> {
    data: &'a [u8],
    count: usize,
    offset: usize,
    idx: usize,
    ctx: container::Ctx,
}

pub type SectionData<'a> = &'a [u8];

impl<'a> ::core::iter::ExactSizeIterator for SectionIterator<'a> {
    fn len(&self) -> usize {
        self.count
    }
}

impl<'a> Iterator for SectionIterator<'a> {
    type Item = error::Result<(Section, SectionData<'a>)>;
    fn next(&mut self) -> Option<Self::Item> {
        if self.idx >= self.count {
            None
        } else {
            self.idx += 1;
            match self.data.gread_with::<Section>(&mut self.offset, self.ctx) {
                Ok(section) => {
                    let section_type = section.flags & SECTION_TYPE;
                    let data = if section_type == S_ZEROFILL
                        || section_type == S_GB_ZEROFILL
                        || section_type == S_THREAD_LOCAL_ZEROFILL
                    {
                        &[]
                    } else {
                        // it's not uncommon to encounter macho files where files are
                        // truncated but the sections are still remaining in the header.
                        // Because of this we want to not panic here but instead just
                        // slice down to a empty data slice.  This way only if code
                        // actually needs to access those sections it will fall over.
                        self.data
                            .get(section.offset as usize..)
                            .unwrap_or_else(|| {
                                warn!(
                                    "section #{} offset {} out of bounds",
                                    self.idx, section.offset
                                );
                                &[]
                            })
                            .get(..section.size as usize)
                            .unwrap_or_else(|| {
                                warn!("section #{} size {} out of bounds", self.idx, section.size);
                                &[]
                            })
                    };
                    Some(Ok((section, data)))
                }
                Err(e) => Some(Err(e)),
            }
        }
    }
}

impl<'a, 'b> IntoIterator for &'b Segment<'a> {
    type Item = error::Result<(Section, SectionData<'a>)>;
    type IntoIter = SectionIterator<'a>;
    fn into_iter(self) -> Self::IntoIter {
        SectionIterator {
            data: self.raw_data,
            count: self.nsects as usize,
            offset: self.offset + Segment::size_with(&self.ctx),
            idx: 0,
            ctx: self.ctx,
        }
    }
}

/// Generalized 32/64 bit Segment Command
pub struct Segment<'a> {
    pub cmd: u32,
    pub cmdsize: u32,
    pub segname: [u8; 16],
    pub vmaddr: u64,
    pub vmsize: u64,
    pub fileoff: u64,
    pub filesize: u64,
    pub maxprot: u32,
    pub initprot: u32,
    pub nsects: u32,
    pub flags: u32,
    pub data: &'a [u8],
    offset: usize,
    raw_data: &'a [u8],
    ctx: container::Ctx,
}

impl<'a> From<Segment<'a>> for SegmentCommand64 {
    fn from(segment: Segment<'a>) -> Self {
        SegmentCommand64 {
            cmd: segment.cmd,
            cmdsize: segment.cmdsize,
            segname: segment.segname,
            vmaddr: segment.vmaddr as u64,
            vmsize: segment.vmsize as u64,
            fileoff: segment.fileoff as u64,
            filesize: segment.filesize as u64,
            maxprot: segment.maxprot,
            initprot: segment.initprot,
            nsects: segment.nsects,
            flags: segment.flags,
        }
    }
}

impl<'a> From<Segment<'a>> for SegmentCommand32 {
    fn from(segment: Segment<'a>) -> Self {
        SegmentCommand32 {
            cmd: segment.cmd,
            cmdsize: segment.cmdsize,
            segname: segment.segname,
            vmaddr: segment.vmaddr as u32,
            vmsize: segment.vmsize as u32,
            fileoff: segment.fileoff as u32,
            filesize: segment.filesize as u32,
            maxprot: segment.maxprot,
            initprot: segment.initprot,
            nsects: segment.nsects,
            flags: segment.flags,
        }
    }
}

impl<'a> fmt::Debug for Segment<'a> {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.debug_struct("Segment")
            .field("cmd", &self.cmd)
            .field("cmdsize", &self.cmdsize)
            .field("segname", &self.segname.pread::<&str>(0).unwrap())
            .field("vmaddr", &self.vmaddr)
            .field("vmsize", &self.vmsize)
            .field("fileoff", &self.fileoff)
            .field("filesize", &self.filesize)
            .field("maxprot", &self.maxprot)
            .field("initprot", &self.initprot)
            .field("nsects", &self.nsects)
            .field("flags", &self.flags)
            .field("data", &self.data.len())
            .field(
                "sections()",
                &self.sections().map(|sections| {
                    sections
                        .into_iter()
                        .map(|(section, _)| section)
                        .collect::<Vec<_>>()
                }),
            )
            .finish()
    }
}

impl<'a> ctx::SizeWith<container::Ctx> for Segment<'a> {
    fn size_with(ctx: &container::Ctx) -> usize {
        match ctx.container {
            container::Container::Little => SIZEOF_SEGMENT_COMMAND_32,
            container::Container::Big => SIZEOF_SEGMENT_COMMAND_64,
        }
    }
}

impl<'a> ctx::TryIntoCtx<container::Ctx> for Segment<'a> {
    type Error = crate::error::Error;
    fn try_into_ctx(self, bytes: &mut [u8], ctx: container::Ctx) -> Result<usize, Self::Error> {
        let segment_size = Self::size_with(&ctx);
        // should be able to write the section data inline after this, but not working at the moment
        //let section_size = bytes.pwrite(data, segment_size)?;
        //debug!("Segment size: {} raw section data size: {}", segment_size, data.len());
        if ctx.is_big() {
            bytes.pwrite_with::<SegmentCommand64>(self.into(), 0, ctx.le)?;
        } else {
            bytes.pwrite_with::<SegmentCommand32>(self.into(), 0, ctx.le)?;
        }
        //debug!("Section size: {}", section_size);
        Ok(segment_size)
    }
}

impl<'a> ctx::IntoCtx<container::Ctx> for Segment<'a> {
    fn into_ctx(self, bytes: &mut [u8], ctx: container::Ctx) {
        bytes.pwrite_with(self, 0, ctx).unwrap();
    }
}

/// Read data that belongs to a segment if the offset is within the boundaries of bytes.
fn segment_data(bytes: &[u8], fileoff: u64, filesize: u64) -> Result<&[u8], error::Error> {
    let data: &[u8] = if filesize != 0 {
        bytes.pread_with(fileoff as usize, filesize as usize)?
    } else {
        &[]
    };
    Ok(data)
}

impl<'a> Segment<'a> {
    /// Create a new, blank segment, with cmd either `LC_SEGMENT_64`, or `LC_SEGMENT`, depending on `ctx`.
    /// **NB** You are responsible for providing a correctly marshalled byte array as the sections. You should not use this for anything other than writing.
    pub fn new(ctx: container::Ctx, sections: &'a [u8]) -> Self {
        Segment {
            cmd: if ctx.is_big() {
                LC_SEGMENT_64
            } else {
                LC_SEGMENT
            },
            cmdsize: (Self::size_with(&ctx) + sections.len()) as u32,
            segname: [0; 16],
            vmaddr: 0,
            vmsize: 0,
            fileoff: 0,
            filesize: 0,
            maxprot: 0,
            initprot: 0,
            nsects: 0,
            flags: 0,
            data: sections,
            offset: 0,
            raw_data: &[],
            ctx,
        }
    }
    /// Get the name of this segment
    pub fn name(&self) -> error::Result<&str> {
        Ok(self.segname.pread::<&str>(0)?)
    }
    /// Get the sections from this segment, erroring if any section couldn't be retrieved
    pub fn sections(&self) -> error::Result<Vec<(Section, SectionData<'a>)>> {
        let mut sections = Vec::new();
        for section in self.into_iter() {
            sections.push(section?);
        }
        Ok(sections)
    }
    /// Convert the raw C 32-bit segment command to a generalized version
    pub fn from_32(
        bytes: &'a [u8],
        segment: &SegmentCommand32,
        offset: usize,
        ctx: container::Ctx,
    ) -> Result<Self, error::Error> {
        Ok(Segment {
            cmd: segment.cmd,
            cmdsize: segment.cmdsize,
            segname: segment.segname,
            vmaddr: u64::from(segment.vmaddr),
            vmsize: u64::from(segment.vmsize),
            fileoff: u64::from(segment.fileoff),
            filesize: u64::from(segment.filesize),
            maxprot: segment.maxprot,
            initprot: segment.initprot,
            nsects: segment.nsects,
            flags: segment.flags,
            data: segment_data(
                bytes,
                u64::from(segment.fileoff),
                u64::from(segment.filesize),
            )?,
            offset,
            raw_data: bytes,
            ctx,
        })
    }
    /// Convert the raw C 64-bit segment command to a generalized version
    pub fn from_64(
        bytes: &'a [u8],
        segment: &SegmentCommand64,
        offset: usize,
        ctx: container::Ctx,
    ) -> Result<Self, error::Error> {
        Ok(Segment {
            cmd: segment.cmd,
            cmdsize: segment.cmdsize,
            segname: segment.segname,
            vmaddr: segment.vmaddr,
            vmsize: segment.vmsize,
            fileoff: segment.fileoff,
            filesize: segment.filesize,
            maxprot: segment.maxprot,
            initprot: segment.initprot,
            nsects: segment.nsects,
            flags: segment.flags,
            data: segment_data(bytes, segment.fileoff, segment.filesize)?,
            offset,
            raw_data: bytes,
            ctx,
        })
    }
}

#[derive(Debug, Default)]
/// An opaque 32/64-bit container for Mach-o segments
pub struct Segments<'a> {
    segments: Vec<Segment<'a>>,
}

impl<'a> Deref for Segments<'a> {
    type Target = Vec<Segment<'a>>;
    fn deref(&self) -> &Self::Target {
        &self.segments
    }
}

impl<'a> DerefMut for Segments<'a> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.segments
    }
}

impl<'a, 'b> IntoIterator for &'b Segments<'a> {
    type Item = &'b Segment<'a>;
    type IntoIter = ::core::slice::Iter<'b, Segment<'a>>;
    fn into_iter(self) -> Self::IntoIter {
        self.segments.iter()
    }
}

impl<'a> Segments<'a> {
    /// Construct a new generalized segment container from this `ctx`
    pub fn new(_ctx: container::Ctx) -> Self {
        Segments {
            segments: Vec::new(),
        }
    }
    /// Get every section from every segment
    // thanks to SpaceManic for figuring out the 'b lifetimes here :)
    pub fn sections<'b>(&'b self) -> Box<dyn Iterator<Item = SectionIterator<'a>> + 'b> {
        Box::new(self.segments.iter().map(|segment| segment.into_iter()))
    }
}
