use std::io::{Read, Seek, SeekFrom, Write};

use crate::error::{Error, Result};
use crate::format::checksum::checksum_metadata;
use crate::format::fixed_array::FixedArrayElement;
use crate::format::object_header;
use crate::io::reader::UNDEF_ADDR;

use super::MutableFile;

impl MutableFile {
    #[allow(clippy::too_many_arguments)]
    pub(super) fn rewrite_fixed_array_chunk(
        &mut self,
        index_addr: u64,
        dataset_addr: u64,
        info: &crate::hl::dataset::DatasetInfo,
        chunk_coords: &[u64],
        chunk_dims: &[u64],
        chunk_size: u64,
        chunk_addr: u64,
        unfiltered_chunk_bytes: usize,
    ) -> Result<()> {
        let element_index =
            Self::linear_chunk_index(chunk_coords, &info.dataspace.dims, chunk_dims)?;
        let filtered = info
            .filter_pipeline
            .as_ref()
            .map(|pipeline| !pipeline.filters.is_empty())
            .unwrap_or(false);
        let chunk_size_len = if filtered {
            Self::filtered_chunk_size_len(
                info.layout.version,
                unfiltered_chunk_bytes,
                self.superblock.sizeof_size,
            )
        } else {
            0
        };

        let mut guard = self.inner.lock();
        if info.dataspace.dims.len() == 1 {
            let mut elements = Vec::new();
            crate::format::fixed_array::read_fixed_array_chunks_into(
                &mut guard.reader,
                index_addr,
                filtered,
                chunk_size_len,
                &mut elements,
            )?;
            if element_index >= elements.len() {
                drop(guard);
                return self.rebuild_grown_1d_fixed_array_chunk_index(
                    index_addr,
                    info,
                    chunk_coords,
                    chunk_dims,
                    chunk_size,
                    chunk_addr,
                    filtered,
                    chunk_size_len,
                    element_index,
                    dataset_addr,
                    elements,
                );
            }
        }
        let element_location =
            crate::format::fixed_array::locate_fixed_array_element_with_checksum(
                &mut guard.reader,
                index_addr,
                filtered,
                chunk_size_len,
                element_index,
            )?;
        drop(guard);

        let sa = usize::from(self.superblock.sizeof_addr);
        self.write_handle
            .seek(SeekFrom::Start(element_location.element_addr))?;
        let mut encoded_addr = [0u8; 8];
        let chunk_addr = Self::encode_uint_le_into(
            chunk_addr,
            &mut encoded_addr,
            sa,
            "fixed array chunk address",
        )?;
        self.write_handle.write_all(chunk_addr)?;
        if filtered {
            self.write_uint_le(chunk_size, chunk_size_len)?;
            self.write_handle.write_all(&0u32.to_le_bytes())?;
        }
        self.rewrite_fixed_array_element_checksum(
            element_location.checksum_start,
            element_location.checksum_len,
            element_location.checksum_addr,
        )?;
        Ok(())
    }

    #[allow(clippy::too_many_arguments)]
    fn rebuild_grown_1d_fixed_array_chunk_index(
        &mut self,
        old_index_addr: u64,
        info: &crate::hl::dataset::DatasetInfo,
        chunk_coords: &[u64],
        chunk_dims: &[u64],
        chunk_size: u64,
        chunk_addr: u64,
        filtered: bool,
        chunk_size_len: usize,
        element_index: usize,
        dataset_addr: u64,
        mut elements: Vec<FixedArrayElement>,
    ) -> Result<()> {
        if chunk_coords.len() != 1 || chunk_dims.len() != 1 || info.dataspace.dims.len() != 1 {
            return Err(Error::Unsupported(
                "fixed-array growth after resize is currently limited to 1D chunk grids".into(),
            ));
        }
        let new_element_count =
            Self::fixed_array_1d_chunk_count(info.dataspace.dims[0], chunk_dims[0])?;
        if element_index >= new_element_count {
            return Err(Error::InvalidFormat(
                "fixed-array chunk coordinate is outside resized extent".into(),
            ));
        }
        if new_element_count <= elements.len() {
            return Err(Error::InvalidFormat(
                "fixed-array rebuild called without growth".into(),
            ));
        }
        elements.resize(
            new_element_count,
            FixedArrayElement {
                addr: UNDEF_ADDR,
                nbytes: if filtered { Some(0) } else { None },
                filter_mask: 0,
            },
        );
        elements[element_index] = FixedArrayElement {
            addr: chunk_addr,
            nbytes: if filtered { Some(chunk_size) } else { None },
            filter_mask: 0,
        };

        let page_bits = self.fixed_array_page_bits_from_layout(dataset_addr)?;
        let new_index_addr =
            self.append_fixed_array_chunk_index(&elements, page_bits, filtered, chunk_size_len)?;
        self.rewrite_fixed_array_layout_index_addr(dataset_addr, old_index_addr, new_index_addr)?;
        Ok(())
    }

    fn fixed_array_1d_chunk_count(dim: u64, chunk: u64) -> Result<usize> {
        if chunk == 0 {
            return Err(Error::InvalidFormat("zero chunk dimension".into()));
        }
        let count = dim
            .checked_add(chunk - 1)
            .ok_or_else(|| Error::InvalidFormat("fixed-array chunk count overflow".into()))?
            / chunk;
        Self::u64_to_usize(count, "fixed-array chunk count")
    }

    fn fixed_array_page_bits_from_layout(&self, dataset_addr: u64) -> Result<u8> {
        let location = self.find_message_in_oh(dataset_addr, object_header::MSG_LAYOUT)?;
        let mut layout = vec![0; location.msg_data_len];
        let layout_pos = self.physical_addr(location.msg_data_offset, "data layout message")?;
        self.read_fresh_bytes_into(layout_pos, &mut layout)?;
        let page_bits_offset = Self::fixed_array_layout_page_bits_offset(&layout)?;
        Ok(layout[page_bits_offset])
    }

    fn fixed_array_layout_page_bits_offset(layout: &[u8]) -> Result<usize> {
        if layout.len() < 7 || layout[0] != 4 || layout[1] != 2 {
            return Err(Error::InvalidFormat(
                "expected v4 chunked fixed-array layout message".into(),
            ));
        }
        let ndims = usize::from(layout[3]);
        let dim_width = usize::from(layout[4]);
        if ndims == 0 || dim_width == 0 || dim_width > 8 {
            return Err(Error::InvalidFormat(
                "invalid fixed-array layout dimension encoding".into(),
            ));
        }
        let index_type_offset = 5usize
            .checked_add(ndims.checked_mul(dim_width).ok_or_else(|| {
                Error::InvalidFormat("fixed-array layout dimension span overflow".into())
            })?)
            .ok_or_else(|| Error::InvalidFormat("fixed-array layout offset overflow".into()))?;
        if layout.get(index_type_offset).copied() != Some(3) {
            return Err(Error::InvalidFormat(
                "layout message is not a fixed-array chunk index".into(),
            ));
        }
        let page_bits_offset = index_type_offset
            .checked_add(1)
            .ok_or_else(|| Error::InvalidFormat("fixed-array layout offset overflow".into()))?;
        if page_bits_offset >= layout.len() {
            return Err(Error::InvalidFormat(
                "fixed-array layout message is truncated".into(),
            ));
        }
        Ok(page_bits_offset)
    }

    fn rewrite_fixed_array_layout_index_addr(
        &mut self,
        dataset_addr: u64,
        old_index_addr: u64,
        new_index_addr: u64,
    ) -> Result<()> {
        let location = self.find_message_in_oh(dataset_addr, object_header::MSG_LAYOUT)?;
        let mut layout = vec![0; location.msg_data_len];
        let layout_pos = self.physical_addr(location.msg_data_offset, "data layout message")?;
        self.read_fresh_bytes_into(layout_pos, &mut layout)?;
        let page_bits_offset = Self::fixed_array_layout_page_bits_offset(&layout)?;
        let addr_offset = page_bits_offset.checked_add(1).ok_or_else(|| {
            Error::InvalidFormat("fixed-array layout address offset overflow".into())
        })?;
        let sa = usize::from(self.superblock.sizeof_addr);
        let addr_end = addr_offset.checked_add(sa).ok_or_else(|| {
            Error::InvalidFormat("fixed-array layout address offset overflow".into())
        })?;
        if addr_end > layout.len() {
            return Err(Error::InvalidFormat(
                "fixed-array layout address field is truncated".into(),
            ));
        }
        let stored_old = Self::decode_uint_le(
            &layout[addr_offset..addr_end],
            "fixed-array layout old address",
        )?;
        if stored_old != old_index_addr {
            return Err(Error::InvalidFormat(
                "fixed-array layout address does not match current chunk index".into(),
            ));
        }

        let mut encoded = [0u8; 8];
        let encoded = Self::encode_uint_le_into(
            new_index_addr,
            &mut encoded,
            sa,
            "fixed-array layout address",
        )?;
        self.write_handle.seek(SeekFrom::Start(
            layout_pos
                .checked_add(Self::usize_to_u64(
                    addr_offset,
                    "fixed-array layout address offset",
                )?)
                .ok_or_else(|| {
                    Error::InvalidFormat("fixed-array layout address position overflow".into())
                })?,
        ))?;
        self.write_handle.write_all(encoded)?;
        if let Some((oh_start, oh_check_len)) = location.v2_checksum {
            self.rewrite_oh_checksum(oh_start, oh_check_len)?;
        }
        Ok(())
    }

    fn append_fixed_array_chunk_index(
        &mut self,
        elements: &[FixedArrayElement],
        page_bits: u8,
        filtered: bool,
        chunk_size_len: usize,
    ) -> Result<u64> {
        if page_bits == 0 {
            return Err(Error::InvalidFormat(
                "fixed-array chunk page bits must be positive".into(),
            ));
        }
        let sa = usize::from(self.superblock.sizeof_addr);
        let ss = usize::from(self.superblock.sizeof_size);
        let page_elements = 1usize.checked_shl(u32::from(page_bits)).ok_or_else(|| {
            Error::InvalidFormat("fixed-array page element count overflow".into())
        })?;
        let raw_element_size = if filtered {
            sa.checked_add(chunk_size_len)
                .and_then(|value| value.checked_add(4))
                .ok_or_else(|| {
                    Error::InvalidFormat("fixed-array raw element size overflow".into())
                })?
        } else {
            sa
        };
        let class_id = if filtered { 1 } else { 0 };
        let header_len = 4usize
            .checked_add(1)
            .and_then(|value| value.checked_add(1))
            .and_then(|value| value.checked_add(1))
            .and_then(|value| value.checked_add(1))
            .and_then(|value| value.checked_add(ss))
            .and_then(|value| value.checked_add(sa))
            .and_then(|value| value.checked_add(4))
            .ok_or_else(|| Error::InvalidFormat("fixed-array header size overflow".into()))?;
        let data_block = self.encode_fixed_array_data_block(
            elements,
            page_bits,
            page_elements,
            raw_element_size,
            class_id,
            filtered,
            chunk_size_len,
            0,
        )?;
        let header_physical = self.append_aligned_zeros(header_len, 8)?;
        let header_addr =
            self.logical_addr_from_physical(header_physical, "fixed-array header address")?;
        let data_physical = self.append_aligned_zeros(data_block.len(), 8)?;
        let data_addr =
            self.logical_addr_from_physical(data_physical, "fixed-array data block address")?;

        let data_block = self.encode_fixed_array_data_block(
            elements,
            page_bits,
            page_elements,
            raw_element_size,
            class_id,
            filtered,
            chunk_size_len,
            header_addr,
        )?;
        self.write_handle.seek(SeekFrom::Start(data_physical))?;
        self.write_handle.write_all(&data_block)?;

        let mut header = Vec::with_capacity(header_len);
        header.extend_from_slice(b"FAHD");
        header.push(0);
        header.push(class_id);
        header.push(
            u8::try_from(raw_element_size).map_err(|_| {
                Error::InvalidFormat("fixed-array raw element size exceeds u8".into())
            })?,
        );
        header.push(page_bits);
        self.append_uint_le_to_vec(
            &mut header,
            Self::usize_to_u64(elements.len(), "fixed-array element count")?,
            ss,
            "fixed-array element count",
        )?;
        self.append_uint_le_to_vec(&mut header, data_addr, sa, "fixed-array data block address")?;
        let checksum = checksum_metadata(&header);
        header.extend_from_slice(&checksum.to_le_bytes());
        self.write_handle.seek(SeekFrom::Start(header_physical))?;
        self.write_handle.write_all(&header)?;
        Ok(header_addr)
    }

    #[allow(clippy::too_many_arguments)]
    fn encode_fixed_array_data_block(
        &self,
        elements: &[FixedArrayElement],
        page_bits: u8,
        page_elements: usize,
        raw_element_size: usize,
        class_id: u8,
        filtered: bool,
        chunk_size_len: usize,
        header_addr: u64,
    ) -> Result<Vec<u8>> {
        let sa = usize::from(self.superblock.sizeof_addr);
        let prefix_payload_len = 4usize
            .checked_add(1)
            .and_then(|value| value.checked_add(1))
            .and_then(|value| value.checked_add(sa))
            .ok_or_else(|| Error::InvalidFormat("fixed-array data block prefix overflow".into()))?;
        let paginated = elements.len() > page_elements;
        let mut data = Vec::new();
        data.extend_from_slice(b"FADB");
        data.push(0);
        data.push(class_id);
        self.append_uint_le_to_vec(
            &mut data,
            header_addr,
            sa,
            "fixed-array data block owner address",
        )?;
        if paginated {
            let page_count = elements.len().div_ceil(page_elements);
            let page_init_len = page_count.div_ceil(8);
            let page_init_start = data.len();
            data.resize(page_init_start + page_init_len, 0);
            for page_index in 0..page_count {
                data[page_init_start + page_index / 8] |= 0x80 >> (page_index % 8);
            }
            let prefix_checksum = checksum_metadata(&data);
            data.extend_from_slice(&prefix_checksum.to_le_bytes());
            for page in elements.chunks(page_elements) {
                let page_start = data.len();
                for element in page {
                    self.append_fixed_array_element(&mut data, element, filtered, chunk_size_len)?;
                }
                let checksum = checksum_metadata(&data[page_start..]);
                data.extend_from_slice(&checksum.to_le_bytes());
            }
        } else {
            debug_assert_eq!(data.len(), prefix_payload_len);
            for element in elements {
                self.append_fixed_array_element(&mut data, element, filtered, chunk_size_len)?;
            }
            let checksum = checksum_metadata(&data);
            data.extend_from_slice(&checksum.to_le_bytes());
        }
        let expected_min = prefix_payload_len
            .checked_add(
                elements
                    .len()
                    .checked_mul(raw_element_size)
                    .ok_or_else(|| {
                        Error::InvalidFormat("fixed-array data block payload overflow".into())
                    })?,
            )
            .ok_or_else(|| Error::InvalidFormat("fixed-array data block size overflow".into()))?;
        if !paginated && data.len() != expected_min + 4 {
            return Err(Error::InvalidFormat(
                "fixed-array data block size accounting mismatch".into(),
            ));
        }
        let _ = page_bits;
        Ok(data)
    }

    fn append_fixed_array_element(
        &self,
        out: &mut Vec<u8>,
        element: &FixedArrayElement,
        filtered: bool,
        chunk_size_len: usize,
    ) -> Result<()> {
        let sa = usize::from(self.superblock.sizeof_addr);
        self.append_uint_le_to_vec(out, element.addr, sa, "fixed-array chunk address")?;
        if filtered {
            self.append_uint_le_to_vec(
                out,
                element.nbytes.unwrap_or(0),
                chunk_size_len,
                "fixed-array filtered chunk size",
            )?;
            out.extend_from_slice(&element.filter_mask.to_le_bytes());
        }
        Ok(())
    }

    fn append_uint_le_to_vec(
        &self,
        out: &mut Vec<u8>,
        value: u64,
        size: usize,
        context: &str,
    ) -> Result<()> {
        let mut bytes = [0u8; 8];
        let bytes = Self::encode_uint_le_into(value, &mut bytes, size, context)?;
        out.extend_from_slice(bytes);
        Ok(())
    }

    fn decode_uint_le(bytes: &[u8], context: &str) -> Result<u64> {
        if bytes.is_empty() || bytes.len() > 8 {
            return Err(Error::InvalidFormat(format!(
                "{context} integer width is invalid"
            )));
        }
        let mut buf = [0u8; 8];
        buf[..bytes.len()].copy_from_slice(bytes);
        Ok(u64::from_le_bytes(buf))
    }

    fn rewrite_fixed_array_element_checksum(
        &mut self,
        checksum_start: u64,
        checksum_len: usize,
        checksum_addr: u64,
    ) -> Result<()> {
        self.write_handle.flush()?;
        let mut reader = std::fs::File::open(&self.path)?;
        reader.seek(SeekFrom::Start(checksum_start))?;
        let mut bytes = vec![0; checksum_len];
        reader.read_exact(&mut bytes)?;
        let checksum = checksum_metadata(&bytes);
        let checksum_end = checksum_addr
            .checked_add(4)
            .ok_or_else(|| Error::InvalidFormat("fixed array checksum address overflow".into()))?;
        let file_len = reader.metadata()?.len();
        if checksum_end > file_len {
            return Err(Error::InvalidFormat(
                "fixed array checksum address exceeds file size".into(),
            ));
        }
        self.write_handle.seek(SeekFrom::Start(checksum_addr))?;
        self.write_handle.write_all(&checksum.to_le_bytes())?;
        Ok(())
    }
}
