// SPDX-License-Identifier: LGPL-3.0-or-later
/*
 * libopenraw - tiff/dir.rs
 *
 * Copyright (C) 2022-2025 Hubert Figuière
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

//! Image File Directory is the main data structure of TIFF used by Exif
//! and most RAW format.

use std::collections::{BTreeMap, HashMap};
use std::io::{Read, Seek, SeekFrom};

use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use log::debug;
use num_enum::TryFromPrimitive;
use once_cell::unsync::OnceCell;

use crate::apple;
use crate::canon;
use crate::container::{self, RawContainer};
use crate::dji;
use crate::epson;
use crate::fujifilm;
use crate::io::{self, View};
use crate::jpeg;
use crate::leica;
use crate::minolta;
use crate::nikon;
use crate::olympus;
use crate::panasonic;
use crate::pentax;
use crate::ricoh;
use crate::sigma;
use crate::sony;
use crate::thumbnail;
use crate::tiff::{
    self,
    exif::{self, TagMap},
    NewSubFileType, TagType,
};
use crate::utils;
use crate::Type as RawType;
use crate::{DataType, Error, Result};

use super::{Entry, Ifd, IfdType};

lazy_static::lazy_static! {
    /// Empty tag list
    static ref MNOTE_EMPTY_TAGS: TagMap = HashMap::new();
}

#[derive(Debug)]
/// IFD
/// Also handle MakerNotes
pub struct Dir {
    /// Endian for the IFD
    pub(crate) endian: container::Endian,
    /// Type of IFD
    pub(crate) type_: IfdType,
    /// All the IFD entries
    pub(crate) entries: BTreeMap<u16, Entry>,
    /// Position of the next IFD
    next: u32,
    /// The dir ID
    pub(crate) id: Vec<u8>,
    /// Offset in MakerNote
    pub mnote_offset: u32,
    /// Tag names to decode.
    pub(crate) tag_names: &'static TagMap,
    /// SubIFs
    sub_ifds: OnceCell<Vec<Dir>>,
}

impl Dir {
    pub(crate) fn iter(&self) -> super::Iterator<'_> {
        super::Iterator::new(self)
    }

    pub(crate) fn create_maker_note(
        container: &crate::tiff::Container,
        offset: u32,
        source: u16,
    ) -> Result<Dir> {
        let file_type = container.raw_type();
        match file_type {
            RawType::Cr2 | RawType::Cr3 | RawType::Crw => {
                return Dir::new_makernote(
                    b"Exif.Canon\0",
                    container,
                    container::Endian::Unset,
                    offset,
                    0,
                    &canon::MNOTE_TAG_NAMES,
                );
            }
            RawType::Dng => {
                if let Some(model) = container
                    .directory(0)
                    .and_then(|dir| dir.entry(exif::EXIF_TAG_MODEL))
                    .and_then(|entry| entry.string_value())
                {
                    if model == "R9 - Digital Back DMR" {
                        return Dir::new_makernote(
                            b"Exif.Leica3\0",
                            container,
                            container::Endian::Unset,
                            offset,
                            0,
                            &leica::MNOTE_TAG_NAMES_3,
                        );
                    }
                }
                if let Some(make) = container
                    .directory(0)
                    .and_then(|dir| dir.entry(exif::EXIF_TAG_MAKE))
                    .and_then(|entry| entry.string_value())
                {
                    if make == "DJI" && source == exif::EXIF_TAG_MAKER_NOTE {
                        return Dir::new_makernote(
                            b"Exif.DJI\0",
                            container,
                            container::Endian::Unset,
                            offset,
                            0,
                            &dji::MNOTE_TAG_NAMES,
                        );
                    }
                }
            }
            _ => {}
        }
        // The size of this buffer should be adjusted depending on
        // what the various detection need.
        let mut data = [0_u8; 19];
        {
            let mut view = container.borrow_view_mut();
            view.seek(SeekFrom::Start(offset as u64))?;
            view.read_exact(&mut data)?;
        }

        log::debug!(
            "MakerNote offset {offset} data {:?}",
            String::from_utf8_lossy(&data)
        );

        if file_type == RawType::Arw {
            let mut padding = 0;
            // A100 has padding.
            if &data[0..9] == b"SONY DSC " {
                padding = 12
            }
            return Dir::new_makernote(
                b"Exif.Sony5\0",
                container,
                container::Endian::Unset,
                offset + padding,
                0,
                &sony::MNOTE_TAG_NAMES,
            );
        }
        if &data[0..6] == b"Nikon\0" {
            if data[6] == 1 {
                return Dir::new_makernote(
                    b"Exif.Nikon2\0",
                    container,
                    container::Endian::Unset,
                    offset + 8,
                    offset + 8,
                    &nikon::MNOTE_TAG_NAMES_2,
                );
            } else if data[6] == 2 {
                // this one has an endian / TIFF header after the magic
                return Dir::new_makernote(
                    b"Exif.Nikon\0",
                    container,
                    container::Endian::Unset,
                    offset + 18,
                    offset + 10,
                    &nikon::MNOTE_TAG_NAMES,
                );
            } else {
                log::error!("Unidentified Nikon makernote");
                return Dir::new_makernote(
                    b"Exif.NikonX\0",
                    container,
                    container::Endian::Unset,
                    offset,
                    offset,
                    &MNOTE_EMPTY_TAGS,
                );
            }
        }
        // Headerless Nikon.
        if file_type == RawType::Nef {
            // Found on D1, D1X, D1H
            return Dir::new_makernote(
                b"Exif.NikonHeaderless\0",
                container,
                container::Endian::Unset,
                offset,
                0,
                &nikon::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..8] == b"OLYMPUS\0" {
            return Dir::new_makernote(
                b"Exif.Olympus2\0",
                container,
                container::Endian::Unset,
                offset + 12,
                offset,
                &olympus::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..9] == b"OM SYSTEM" {
            return Dir::new_makernote(
                b"Exif.Olympus2\0",
                container,
                container::Endian::Unset,
                offset + 16,
                offset,
                &olympus::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..6] == b"OLYMP\0" {
            return Dir::new_makernote(
                b"Exif.Olympus\0",
                container,
                container::Endian::Unset,
                offset + 8,
                0,
                &olympus::MNOTE_TAG_NAMES,
            );
        }

        // EPSON R-D1, use Olympus
        // XXX deal with endian.
        if &data[0..6] == b"EPSON\0" {
            return Dir::new_makernote(
                b"Exif.Epson\0",
                container,
                container::Endian::Unset,
                offset + 8,
                0,
                &epson::MNOTE_TAG_NAMES,
            );
        }

        // Pentax Asahi Optical Corporation (pre Ricoh merger)
        if &data[0..4] == b"AOC\0" {
            return Dir::new_makernote(
                b"Exif.Pentax\0",
                container,
                container::Endian::Unset,
                offset + 6,
                0,
                &pentax::MNOTE_TAG_NAMES,
            );
        }

        // Pentax post Ricoh merger
        if &data[0..8] == b"PENTAX \0" {
            return Dir::new_makernote(
                b"Exif.Pentax\0",
                container,
                container::Endian::Unset,
                offset + 10,
                offset,
                &pentax::MNOTE_TAG_NAMES,
            );
        }

        // Ricoh post Pentax merger
        if &data[0..6] == b"RICOH\0" {
            return Dir::new_makernote(
                b"Exif.Pentax\0",
                container,
                container::Endian::Unset,
                offset + 8,
                offset + 8,
                &pentax::MNOTE_TAG_NAMES,
            );
        }

        // Panasonic
        if &data[0..10] == b"Panasonic\0" {
            return Dir::new_makernote(
                b"Exif.Panasonic\0",
                container,
                container::Endian::Unset,
                offset + 12,
                0,
                &panasonic::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..6] == b"Ricoh\0" {
            return Dir::new_makernote(
                b"Exif.Ricoh\0",
                container,
                container::Endian::Unset,
                offset + 8,
                0,
                &ricoh::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..16] == b"LEICA CAMERA AG\0" {
            if file_type == RawType::Rw2 {
                // Rebadged Panasonic
                // Leica C-Lux
                // Leica V-Lux 5
                // Leica D-Lux 7
                return Dir::new_makernote(
                    b"Exif.Panasonic\0",
                    container,
                    container::Endian::Unset,
                    offset + 18,
                    0,
                    &panasonic::MNOTE_TAG_NAMES,
                );
            } else if file_type == RawType::Dng {
                // Leica D-Lux 8 (DNG)
                return Dir::new_makernote(
                    b"Exif.Leica5\0",
                    container,
                    container::Endian::Unset,
                    offset + 18,
                    0,
                    &leica::MNOTE_TAG_NAMES_5,
                );
            }
        }

        if &data[0..5] == b"LEICA" {
            if &data[5..8] == b"\0\0\0" {
                if file_type == RawType::Rw2 {
                    // Panasonic
                    return Dir::new_makernote(
                        b"Exif.Panasonic\0",
                        container,
                        container::Endian::Unset,
                        offset + 8,
                        0,
                        &panasonic::MNOTE_TAG_NAMES,
                    );
                } else {
                    // Leica M8
                    return Dir::new_makernote(
                        b"Exif.Leica2\0",
                        container,
                        container::Endian::Unset,
                        offset + 8,
                        offset,
                        &leica::MNOTE_TAG_NAMES_2,
                    );
                }
            }

            if data[5] == 0 && data[7] == 0 {
                match data[6] {
                            0x08..=0x0a =>
                            // Leica Q Typ 116, SL (Type 601), Q3 (0x0a)
                                return Dir::new_makernote(
                                    b"Exif.Leica5\0",
                                    container,
                                    container::Endian::Unset,
                                    offset + 8, 0,
                                    &leica::MNOTE_TAG_NAMES_5,
                                ),
                            0x01 | // Leica X1
                            0x04 | // Leica X VARIO
                            0x05 | // Leica X2
                            0x06 | // Leica T (Typ 701)
                            0x07 | // Leica X (Typ 113)
                            0x10 | // Leica X-U (Typ 113)
                            0x1a =>
                                return Dir::new_makernote(
                                    b"Leica5\0",
                                    container,
                                    container::Endian::Unset,
                                    offset + 8,
                                    offset,
                                    &leica::MNOTE_TAG_NAMES_5,
                                ),
                            _ => {}
                        }
            }

            // Leica M (Typ 240)
            if data[5] == 0x0 && data[6] == 0x02 && data[7] == 0xff {
                return Dir::new_makernote(
                    b"Exif.Leica6\0",
                    container,
                    container::Endian::Unset,
                    offset + 8,
                    0,
                    &leica::MNOTE_TAG_NAMES_6,
                );
            }

            // Leica M9/Monochrom
            if data[5] == b'0' && data[6] == 0x03 && data[7] == 0 {
                return Dir::new_makernote(
                    b"Exif.Leica4\0",
                    container,
                    container::Endian::Unset,
                    offset + 8,
                    offset,
                    &leica::MNOTE_TAG_NAMES_4,
                );
            }

            // Leica M10
            if data[5] == 0 && data[6] == 0x02 && data[7] == 0 {
                return Dir::new_makernote(
                    b"Exif.Leica9\0",
                    container,
                    container::Endian::Unset,
                    offset + 8,
                    0,
                    &leica::MNOTE_TAG_NAMES_9,
                );
            }
        }

        if &data[0..8] == b"YI     \0" {
            return Dir::new_makernote(
                b"Exif.Xiaoyi\0",
                container,
                container::Endian::Unset,
                offset + 12,
                offset,
                // XXX we have no idea.
                &MNOTE_EMPTY_TAGS,
            );
        }

        if &data[0..10] == b"Apple iOS\0" {
            return Dir::new_makernote(
                b"Exif.Apple\0",
                container,
                container::Endian::Unset,
                offset + 14,
                offset,
                &apple::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..4] == b"STMN" {
            return if &data[8..12] == b"\0\0\0\0" {
                Dir::new_makernote(
                    b"Exif.Samsung1a\0",
                    container,
                    container::Endian::Unset,
                    offset,
                    offset,
                    &MNOTE_EMPTY_TAGS,
                )
            } else {
                Dir::new_makernote(
                    b"Exif.Samsung1b\0",
                    container,
                    container::Endian::Unset,
                    offset,
                    offset,
                    &MNOTE_EMPTY_TAGS,
                )
            };
        }

        if &data[0..8] == b"samsung:" {
            log::debug!("samsung: MakerNote (Unknown)");
            return Err(Error::UnsupportedMakerNote);
        }

        if &data[0..8] == b"FUJIFILM" {
            return Dir::new_makernote(
                b"Exif.Fujifilm\0",
                container,
                container::Endian::Little,
                offset + 12,
                offset,
                &fujifilm::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..6] == b"SIGMA\0" {
            return Dir::new_makernote(
                b"Exif.Sigma\0",
                container,
                container::Endian::Unset,
                offset + 10,
                0,
                &sigma::MNOTE_TAG_NAMES,
            );
        }

        // This is the Maker Note found in the Sigma fp
        // that also has the regular one in the DNG Private.
        if &data[0..16] == b"SIGMA_EXTH_SPPA\0" {
            return Err(Error::UnsupportedMakerNote);
        }

        if &data[10..14] == b"MLT0" {
            return Dir::new_makernote(
                b"Exif.Minolta\0",
                container,
                container::Endian::Unset,
                offset,
                offset,
                &minolta::MNOTE_TAG_NAMES,
            );
        }

        if &data[0..6] == b"Nokia\0" {
            log::debug!("Nokia MakerNote (Unknown)");
            return Err(Error::UnsupportedMakerNote);
        }

        if &data[0..6] == b"Adobe\0" {
            log::debug!("Adobe Converted MakerNote");
            return Err(Error::UnsupportedMakerNote);
        }

        // Others

        // Seitz 617, unknown format, doesn't look like an IFD.
        if &data[0..19] == b"SEITZ PHOTOTECHNIK\0" {
            return Err(Error::UnsupportedMakerNote);
        }
        // SilverFast scanner. (Plustek)
        // Doesn't follow the standard.
        if &data[0..5] == b"LSI1\0" {
            return Err(Error::UnsupportedMakerNote);
        }
        // Google Pixel
        // BLOB
        if &data[0..4] == b"HDRP" {
            return Err(Error::UnsupportedMakerNote);
        }
        // Hasselblad/DJI
        // BLOB
        if &data[0..13] == b"[ae_dbg_info:" {
            return Err(Error::UnsupportedMakerNote);
        }

        // Insta360
        if &data[0..19] == b"2_1483.473_1528.204" {
            return Err(Error::UnsupportedMakerNote);
        }

        // Default in DNGPrivate isn't an IFD.
        if source == exif::DNG_TAG_DNG_PRIVATE {
            return Err(Error::UnsupportedMakerNote);
        }

        Dir::new_makernote(
            b"\0",
            container,
            container::Endian::Unset,
            offset,
            0,
            &MNOTE_EMPTY_TAGS,
        )
    }

    /// Create a new MakerNote with `id`. `offset` is the offset in the container
    /// while `mnote_offset` is the offset to apply from inside the MakerNote.
    /// `tag_names` allow custom display of tags.
    /// Passing `container::Endian::Unset` for `endian` mean that the endian from
    /// container shall be used. It's the default behaviour. But sometime it is necessary
    /// to do expicitely override the endian like Fujifilm.
    pub(crate) fn new_makernote(
        id: &[u8],
        container: &dyn container::RawContainer,
        endian: container::Endian,
        offset: u32,
        mnote_offset: u32,
        tag_names: &'static TagMap,
    ) -> Result<Dir> {
        let endian = if endian == container::Endian::Unset {
            container.endian()
        } else {
            endian
        };
        if let Ok(mut dir) = match endian {
            container::Endian::Little => {
                let mut view = container.borrow_view_mut();
                Dir::read::<LittleEndian>(&mut view, offset, mnote_offset, IfdType::MakerNote)
            }
            container::Endian::Big => {
                let mut view = container.borrow_view_mut();
                Dir::read::<BigEndian>(&mut view, offset, mnote_offset, IfdType::MakerNote)
            }
            _ => {
                log::error!("Endian unset to read directory");
                Err(Error::NotFound)
            }
        } {
            dir.id = id.to_vec();
            dir.mnote_offset = mnote_offset;
            dir.tag_names = tag_names;
            Ok(dir)
        } else {
            Err(Error::NotFound)
        }
    }

    /// Read an IFD from the view, using endian `E`.
    /// `base_offset` is the offset for reading data, like it is used in
    /// MakerNote. 0 is the default.
    pub(crate) fn read<E>(
        view: &mut View,
        dir_offset: u32,
        base_offset: u32,
        type_: IfdType,
    ) -> Result<Self>
    where
        E: container::EndianType,
    {
        let mut entries = BTreeMap::new();
        view.seek(SeekFrom::Start(dir_offset as u64))?;
        debug!("Dir starts at {dir_offset}");
        let num_entries = view.read_i16::<E>()?;
        for _ in 0..num_entries {
            let id = view.read_u16::<E>()?;
            let type_ = view.read_i16::<E>()?;
            let count = view.read_u32::<E>()?;
            let mut data = [0_u8; 4];
            view.read_exact(&mut data)?;
            debug!("Entry {id:x} with type {type_} count {count} added");
            let mut entry = Entry::new(id, type_, count, data);
            if !entry.is_inline() {
                let pos = view.stream_position()?;
                let r = entry.load_data::<E>(base_offset, view);
                view.seek(SeekFrom::Start(pos))?;
                if r.is_err() {
                    // We'll just stop parsing.
                    // This is encountered on Somy A100 and is likely
                    // a bug, but it's also in the C++ code.
                    log::error!(
                        "Skipping entry {id:x} at {pos} with count {count}, Setting type to Error"
                    );
                    entry.type_ = TagType::Error_ as i16;
                }
            }
            entries.insert(id, entry);
        }

        let next = view.read_u32::<E>()?;
        Ok(Dir {
            endian: E::ENDIAN,
            type_,
            entries,
            next,
            id: vec![0],
            mnote_offset: base_offset,
            tag_names: &exif::TAG_NAMES,
            sub_ifds: OnceCell::new(),
        })
    }

    pub(crate) fn new(endian: container::Endian, type_: IfdType) -> Self {
        Dir {
            endian,
            type_,
            entries: BTreeMap::new(),
            id: vec![0],
            next: 0,
            mnote_offset: 0,
            tag_names: &exif::TAG_NAMES,
            sub_ifds: OnceCell::new(),
        }
    }

    pub fn id(&self) -> &[u8] {
        &self.id
    }

    /// Offset of the next IFD. 0 mean this was the last one.
    pub fn next_ifd(&self) -> u32 {
        self.next
    }

    /// Get and unsigned integer that could be either size.
    pub fn uint_value(&self, tag: u16) -> Option<u32> {
        self.entry(tag).and_then(|e| match self.endian() {
            container::Endian::Little => e.uint_value::<LittleEndian>(),
            container::Endian::Big => e.uint_value::<BigEndian>(),
            _ => {
                log::error!("Endian unset to read directory");
                None
            }
        })
    }

    /// Get an array of unsigned integers that could be either size.
    pub fn uint_value_array(&self, tag: u16) -> Option<Vec<u32>> {
        self.entry(tag)
            .and_then(|e| e.uint_value_array(self.endian()))
    }

    /// Get an array of u16 integers.
    pub fn u16_value_array(&self, tag: u16) -> Option<Vec<u16>> {
        self.entry(tag)
            .and_then(|e| e.value_array::<u16>(self.endian()))
    }

    /// Get an array of signed integers that could be either size.
    pub fn int_value_array(&self, tag: u16) -> Option<Vec<i32>> {
        self.entry(tag)
            .and_then(|e| e.int_value_array(self.endian()))
    }

    /// Get an array of floats that could be either size.
    pub fn float_value_array(&self, tag: u16) -> Option<Vec<f64>> {
        self.entry(tag)
            .and_then(|e| e.float_value_array(self.endian()))
    }

    /// Whether the IFD is primary
    pub fn is_primary(&self) -> bool {
        if let Some(v) = self.value::<u32>(exif::EXIF_TAG_NEW_SUBFILE_TYPE) {
            v == 0
        } else {
            false
        }
    }

    /// Get the Exif IFD from the directory
    pub(crate) fn get_exif_ifd(&self, container: &tiff::Container) -> Option<Dir> {
        self.value::<u32>(exif::EXIF_TAG_EXIF_IFD_POINTER)
            .and_then(|offset| {
                let mut view = container.borrow_view_mut();
                container
                    .dir_at(
                        &mut view,
                        offset,
                        self.mnote_offset,
                        IfdType::Exif,
                        Some("Exif"),
                        None,
                    )
                    .map_err(|e| {
                        log::warn!("Coudln't get exif dir at {offset}: {e}");
                        e
                    })
                    .ok()
            })
    }

    /// Get the Maker Note IFD from the `tag`.
    pub(crate) fn mnote_ifd_for_tag(&self, container: &tiff::Container, tag: u16) -> Option<Dir> {
        self.entry(tag).and_then(|e| e.offset()).and_then(|offset| {
            Dir::create_maker_note(container, offset, tag)
                .map_err(|e| {
                    log::warn!("Coudln't create maker_note: {e}");
                    e
                })
                .ok()
        })
    }

    /// Get the Maker Note IFD from the directory
    pub(crate) fn get_mnote_ifd(&self, container: &tiff::Container) -> Option<Dir> {
        self.mnote_ifd_for_tag(container, exif::EXIF_TAG_MAKER_NOTE)
    }

    /// Get the Maker Note IFD from the directory
    pub(crate) fn get_dng_private_ifd(&self, container: &tiff::Container) -> Option<Dir> {
        self.mnote_ifd_for_tag(container, exif::DNG_TAG_DNG_PRIVATE)
    }

    /// Get the GPSInfo IFD from the directory
    pub(crate) fn get_gpsinfo_ifd(&self, container: &tiff::Container) -> Option<Dir> {
        self.value::<u32>(exif::EXIF_TAG_GPS_INFO_IFD_POINTER)
            .and_then(|offset| {
                let mut view = container.borrow_view_mut();
                container
                    .dir_at(
                        &mut view,
                        offset,
                        self.mnote_offset,
                        IfdType::GpsInfo,
                        Some("GPSInfo"),
                        Some(&exif::GPSINFO_TAG_NAMES),
                    )
                    .map_err(|e| {
                        log::warn!("Coudln't get GPSInfo dir at {offset}: {e}");
                        e
                    })
                    .ok()
            })
    }

    /// Get the TIFF stored in the entry
    ///
    /// XXX find a way to initialize the id.
    pub(crate) fn tiff_in_entry(
        &self,
        container: &tiff::Container,
        tag: u16,
        tag_names: Option<&'static TagMap>,
    ) -> Option<tiff::Container> {
        self.entry(tag).and_then(|e| e.offset()).and_then(|offset| {
            let view = container.borrow_view_mut();
            crate::io::Viewer::create_subview(&view, offset as u64)
                .ok()
                .and_then(|view| {
                    let mut container = tiff::Container::new(
                        view,
                        vec![(tiff::IfdType::Main, tag_names)],
                        RawType::Unknown,
                    );
                    container.load(None).ok()?;
                    Some(container)
                })
        })
    }

    /// Get the IFD stored in the entry
    pub(crate) fn ifd_in_entry(
        &self,
        container: &tiff::Container,
        tag: u16,
        id: Option<&'static str>,
        tag_names: Option<&'static TagMap>,
    ) -> Option<Dir> {
        self.entry(tag).and_then(|e| {
            if e.type_ == 13 || e.type_ == exif::TagType::Long as i16 {
                match self.endian() {
                    container::Endian::Little => e.value_untyped::<u32, LittleEndian>(),
                    container::Endian::Big => e.value_untyped::<u32, BigEndian>(),
                    _ => {
                        log::error!("Endian unset to read directory");
                        return None;
                    }
                }
                .map(|offset| (offset as i64 + self.mnote_offset as i64) as u32)
            } else {
                e.offset()
            }
            .and_then(|val_offset| {
                let mut view = container.borrow_view_mut();
                container
                    .dir_at(
                        &mut view,
                        val_offset,
                        self.mnote_offset,
                        IfdType::Other,
                        id,
                        tag_names,
                    )
                    .ok()
            })
        })
    }

    // Locate a JPEG XL bitstream.
    fn locate_jxl(&self) -> Option<thumbnail::DataOffset> {
        let len = self.uint_value_array(exif::TIFF_TAG_TILE_BYTECOUNTS)?;
        let offset = self.uint_value_array(exif::TIFF_TAG_TILE_OFFSETS)?;
        if len.len() > 1 || offset.len() > 1 {
            return None;
        }

        Some(thumbnail::DataOffset {
            offset: offset[0] as u64,
            len: len[0] as u64,
        })
    }

    // Locate a JPEG bitstream
    fn locate_jpeg(&self) -> Option<thumbnail::DataOffset> {
        let len = self.value::<u32>(exif::EXIF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)? as u64;
        let offset = self.value::<u32>(exif::EXIF_TAG_JPEG_INTERCHANGE_FORMAT)? as u64;

        Some(thumbnail::DataOffset { offset, len })
    }

    pub(crate) fn locate_thumbnail(
        &self,
        container: &dyn RawContainer,
        thumbnails: &mut Vec<(u32, thumbnail::ThumbDesc)>,
    ) {
        let mut data_type = DataType::Unknown;
        let subtype = self
            .value::<u32>(exif::EXIF_TAG_NEW_SUBFILE_TYPE)
            .and_then(|v| NewSubFileType::try_from_primitive(v).ok())
            .unwrap_or(NewSubFileType::Preview);
        if subtype == NewSubFileType::Preview {
            let photom_int = self
                .value::<u16>(exif::EXIF_TAG_PHOTOMETRIC_INTERPRETATION)
                .and_then(|v| exif::PhotometricInterpretation::try_from_primitive(v as u32).ok())
                .unwrap_or(exif::PhotometricInterpretation::Rgb);
            let mut x = self.uint_value(exif::EXIF_TAG_IMAGE_WIDTH).unwrap_or(0);
            let mut y = self.uint_value(exif::EXIF_TAG_IMAGE_LENGTH).unwrap_or(0);
            let compression = self.value::<u16>(exif::EXIF_TAG_COMPRESSION).unwrap_or(0);
            let mut byte_count = self
                .value::<u32>(exif::EXIF_TAG_STRIP_BYTE_COUNTS)
                .unwrap_or(0);
            let mut offset = 0;
            let mut got_it = false;
            if let Some(v) = self.value::<u32>(exif::EXIF_TAG_STRIP_OFFSETS) {
                offset = v;
                got_it = true;
            }
            if !got_it || compression == 6 || compression == 7 || compression == 52546 {
                data_type = match compression {
                    6 | 7 => DataType::Jpeg,
                    52546 => DataType::Jxl,
                    // That is not unreachable because some CR2
                    // thumbnails have no compression marker.
                    _ => DataType::Unknown,
                };
                if !got_it {
                    if let Some(data_offset) = self.locate_jpeg() {
                        byte_count = data_offset.len as u32;
                        offset = data_offset.offset as u32;
                        data_type = DataType::Jpeg;
                        got_it = true;
                    } else if let Some(data_offset) = self.locate_jxl() {
                        byte_count = data_offset.len as u32;
                        offset = data_offset.offset as u32;
                        got_it = true;
                    } else {
                        data_type = DataType::Unknown;
                    }
                }
                if got_it {
                    // workaround for CR2 files where 8RGB data is marked
                    // as JPEG. Check the real data size.
                    if x != 0 && y != 0 {
                        let pixel_count = x.checked_mul(y).and_then(|count| count.checked_mul(3));
                        if pixel_count.is_none() || byte_count >= pixel_count.unwrap() {
                            // We ignore this, it's 8RGB in a Canon CR2 file.
                            // See bug 72270
                            log::debug!("8RGB as JPEG or invalid pixel count. Will ignore.");
                            data_type = DataType::Unknown
                        }
                    } else {
                        // What do we do with JXL? This might only apply
                        // for JPEG.
                        if x == 0 || y == 0 {
                            if let Ok(view) = io::Viewer::create_subview(
                                &container.borrow_view_mut(),
                                offset as u64,
                            ) {
                                let jpeg = jpeg::Container::new(view, container.raw_type());
                                x = jpeg.width() as u32;
                                y = jpeg.height() as u32;
                                log::debug!("Found JPEG dimensions x={x} y={y}");
                            } else {
                                // XXX load the JFIF stream and get the dimensions.
                                log::error!("Couldn't get JPEG dimensions.");
                            }
                        } else {
                            log::debug!("JPEG (supposed) dimensions x={x} y={y}");
                        }
                    }
                }
            } else if photom_int == exif::PhotometricInterpretation::YCbCr {
                log::warn!("Unsupported YCbCr photometric interpretation in non JPEG.");
            } else {
                log::debug!("Found strip offsets");
                if x != 0 && y != 0 {
                    // See bug 72270 - some CR2 have 16 bpc RGB thumbnails.
                    // by default it is RGB8. Unless stated otherwise.
                    let mut is_rgb8 = true;
                    if let Some(arr) = self
                        .u16_value_array(exif::EXIF_TAG_BITS_PER_SAMPLE)
                        .or_else(|| {
                            log::debug!("Error getting bpc");
                            None
                        })
                    {
                        for bpc in arr {
                            is_rgb8 = bpc == 8;
                            if !is_rgb8 {
                                log::debug!("bpc != 8 {bpc}");
                                break;
                            }
                        }
                    }
                    if is_rgb8 {
                        data_type = DataType::PixmapRgb8;
                    }
                }
            }
            if data_type != DataType::Unknown {
                let dim = std::cmp::max(x, y);
                if dim > 0 {
                    // XXX compute
                    // offset += offset();
                    let desc = thumbnail::ThumbDesc {
                        width: x,
                        height: y,
                        data_type,
                        data: thumbnail::Data::Offset(thumbnail::DataOffset {
                            offset: offset as u64,
                            len: byte_count as u64,
                        }),
                    };
                    thumbnails.push((dim, desc));
                }
            }
        }
    }

    /// Get sub IFDs. The vec is empty if none is found.
    pub(crate) fn get_sub_ifds(&self, container: &tiff::Container) -> &Vec<Dir> {
        self.sub_ifds.get_or_init(|| {
            let mut ifds = Vec::new();
            if let Some(offsets) = self
                .entry(exif::EXIF_TAG_SUB_IFDS)
                .and_then(|entry| entry.value_array::<u32>(self.endian()))
            {
                if let Some(fixup) = &container.loader_fixup {
                    if !fixup.parse_subifd(container) {
                        return ifds;
                    }
                }
                for offset in offsets {
                    if offset == 0 {
                        log::warn!("SubIFD with offset 0");
                        continue;
                    }
                    if let Ok(dir) = match self.endian() {
                        container::Endian::Little => {
                            let mut view = container.borrow_view_mut();
                            Dir::read::<LittleEndian>(
                                &mut view,
                                offset,
                                self.mnote_offset,
                                IfdType::SubIfd,
                            )
                        }
                        container::Endian::Big => {
                            let mut view = container.borrow_view_mut();
                            Dir::read::<BigEndian>(
                                &mut view,
                                offset,
                                self.mnote_offset,
                                IfdType::SubIfd,
                            )
                        }
                        _ => {
                            log::error!("Endian unset to read directory");
                            Err(Error::NotFound)
                        }
                    } {
                        ifds.push(dir);
                    };
                }
            }
            ifds
        })
    }

    pub fn tag_name(&self, id: u16) -> &'static str {
        self.tag_names.get(&id).unwrap_or(&"")
    }

    #[cfg(feature = "dump")]
    pub(crate) fn write_dump_dir<W>(&self, out: &mut W, container: &tiff::Container, indent: u32)
    where
        W: std::io::Write + ?Sized,
    {
        let id = utils::from_maybe_nul_terminated(&self.id);
        dump_writeln!(
            out,
            indent,
            "<IFD type={:?} id=\"{}\" {} entries next=@{}>",
            self.type_,
            id,
            self.num_entries(),
            self.next_ifd()
        );
        {
            let indent = indent + 1;
            for (id, entry) in &self.entries {
                let tag_name = self.tag_name(*id);
                let args = HashMap::from([("tag_name", String::from(tag_name))]);
                entry.write_dump_entry(out, indent, self.endian(), args);
            }
            if let Some(exif_dir) = self.get_exif_ifd(container) {
                exif_dir.write_dump_dir(out, container, indent);
            }
            if let Some(mnote_dir) = self.get_mnote_ifd(container) {
                mnote_dir.write_dump_dir(out, container, indent);
            }
            if let Some(dng_private_data) = self.get_dng_private_ifd(container) {
                dng_private_data.write_dump_dir(out, container, indent);
            }
            if let Some(gps_dir) = self.get_gpsinfo_ifd(container) {
                gps_dir.write_dump_dir(out, container, indent);
            }
            let subdirs = self.get_sub_ifds(container);
            for subdir in subdirs {
                subdir.write_dump_dir(out, container, indent);
            }
        }
        dump_writeln!(out, indent, "</IFD>");
    }
}

impl Ifd for Dir {
    fn ifd_type(&self) -> IfdType {
        self.type_
    }

    fn endian(&self) -> container::Endian {
        self.endian
    }

    /// Return the number of entries.
    fn num_entries(&self) -> usize {
        self.entries.len()
    }

    /// Return the entry for the `tag`.
    fn entry(&self, tag: u16) -> Option<&Entry> {
        self.entries.get(&tag)
    }
}

#[cfg(test)]
mod test {
    use log;
    use simple_logger;

    use crate::io::Viewer;
    use crate::metadata::Value;
    use crate::tiff;
    use crate::tiff::exif;
    use crate::Type;

    #[test]
    fn test_ifd_iterator() {
        simple_logger::init_with_level(log::Level::Debug).unwrap();

        let reader = std::fs::File::open("test/iterator_test.tif").expect("Couldn't open file");
        let viewer = Viewer::new(Box::new(std::io::BufReader::new(reader)), 0);
        let view = Viewer::create_view(&viewer, 0).expect("Couldn't create view");
        let mut container =
            tiff::Container::new(view, vec![(tiff::IfdType::Main, None)], Type::Jpeg);
        container.load(None).expect("Failed to load IFD");
        let dir = container.directory(0).expect("Couldn't get directory");
        let mut iter = dir.iter();

        let meta = iter.next().expect("Couldn't get first item");
        assert_eq!(meta.0, "Exif.Image.SubfileType");
        assert_eq!(meta.1, Value::Int(vec![0]));
        assert_eq!(meta.2, exif::TagType::Long as i16);

        let meta = iter.next().expect("Couldn't get second item");
        assert_eq!(meta.0, "Exif.Image.ImageWidth");
        assert_eq!(meta.1, Value::Int(vec![360]));
        assert_eq!(meta.2, exif::TagType::Short as i16);
    }
}
