// ***************************************************************** -*- C++ -*-
/*
 * Copyright (C) 2004-2021 Exiv2 authors
 * This program is part of the Exiv2 distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, 5th Floor, Boston, MA 02110-1301 USA.
 */
// *****************************************************************************
// included header files
#include "config.h"

#include "basicio.hpp"
#include "enforce.hpp"
#include "error.hpp"
#include "futils.hpp"
#include "helper_functions.hpp"
#include "quicktimevideo.hpp"
#include "safe_op.hpp"
#include "tags.hpp"
#include "tags_int.hpp"
// + standard includes
#include <array>
#include <cmath>
#include <iostream>
#include <string>
// *****************************************************************************
// class member definitions
namespace Exiv2::Internal {

static constexpr TagVocabulary qTimeFileType[] = {
    {"3g2a", "3GPP2 Media (.3G2) compliant with 3GPP2 C.S0050-0 V1.0"},
    {"3g2b", "3GPP2 Media (.3G2) compliant with 3GPP2 C.S0050-A V1.0.0"},
    {"3g2c", "3GPP2 Media (.3G2) compliant with 3GPP2 C.S0050-B v1.0"},
    {"3ge6", "3GPP (.3GP) Release 6 MBMS Extended Presentations"},
    {"3ge7", "3GPP (.3GP) Release 7 MBMS Extended Presentations"},
    {"3gg6", "3GPP Release 6 General Profile"},
    {"3gp1", "3GPP Media (.3GP) Release 1 (probably non-existent)"},
    {"3gp2", "3GPP Media (.3GP) Release 2 (probably non-existent)"},
    {"3gp3", "3GPP Media (.3GP) Release 3 (probably non-existent)"},
    {"3gp4", "3GPP Media (.3GP) Release 4"},
    {"3gp5", "3GPP Media (.3GP) Release 5"},
    {"3gp6", "3GPP Media (.3GP) Release 6 Streaming Servers"},
    {"3gs7", "3GPP Media (.3GP) Release 7 Streaming Servers"},
    {"CAEP", "Canon Digital Camera"},
    {"CDes", "Convergent Design"},
    {"F4A ", "Audio for Adobe Flash Player 9+ (.F4A)"},
    {"F4B ", "Audio Book for Adobe Flash Player 9+ (.F4B)"},
    {"F4P ", "Protected Video for Adobe Flash Player 9+ (.F4P)"},
    {"F4V ", "Video for Adobe Flash Player 9+ (.F4V)"},
    {"JP2 ", "JPEG 2000 Image (.JP2) [ISO 15444-1 ?]"},
    {"JP20", "Unknown, from GPAC samples (prob non-existent)"},
    {"KDDI", "3GPP2 EZmovie for KDDI 3G cellphones"},
    {"M4A ", "Apple iTunes AAC-LC (.M4A) Audio"},
    {"M4B ", "Apple iTunes AAC-LC (.M4B) Audio Book"},
    {"M4P ", "Apple iTunes AAC-LC (.M4P) AES Protected Audio"},
    {"M4V ", "Apple iTunes Video (.M4V) Video"},
    {"M4VH", "Apple TV (.M4V)"},
    {"M4VP", "Apple iPhone (.M4V)"},
    {"MPPI", "Photo Player, MAF [ISO/IEC 23000-3]"},
    {"MSNV", "MPEG-4 (.MP4) for SonyPSP"},
    {"NDAS", "MP4 v2 [ISO 14496-14] Nero Digital AAC Audio"},
    {"NDSC", "MPEG-4 (.MP4) Nero Cinema Profile"},
    {"NDSH", "MPEG-4 (.MP4) Nero HDTV Profile"},
    {"NDSM", "MPEG-4 (.MP4) Nero Mobile Profile"},
    {"NDSP", "MPEG-4 (.MP4) Nero Portable Profile"},
    {"NDSS", "MPEG-4 (.MP4) Nero Standard Profile"},
    {"NDXC", "H.264/MPEG-4 AVC (.MP4) Nero Cinema Profile"},
    {"NDXH", "H.264/MPEG-4 AVC (.MP4) Nero HDTV Profile"},
    {"NDXM", "H.264/MPEG-4 AVC (.MP4) Nero Mobile Profile"},
    {"NDXP", "H.264/MPEG-4 AVC (.MP4) Nero Portable Profile"},
    {"NDXS", "H.264/MPEG-4 AVC (.MP4) Nero Standard Profile"},
    {"NIKO", "Nikon"},
    {"ROSS", "Ross Video"},
    {"avc1", "MP4 Base w/ AVC ext [ISO 14496-12:2005]"},
    {"caqv", "Casio Digital Camera"},
    {"da0a", "DMB MAF w/ MPEG Layer II aud, MOT slides, DLS, JPG/PNG/MNG images"},
    {"da0b", "DMB MAF, extending DA0A, with 3GPP timed text, DID, TVA, REL, IPMP"},
    {"da1a", "DMB MAF audio with ER-BSAC audio, JPG/PNG/MNG images"},
    {"da1b", "DMB MAF, extending da1a, with 3GPP timed text, DID, TVA, REL, IPMP"},
    {"da2a", "DMB MAF aud w/ HE-AAC v2 aud, MOT slides, DLS, JPG/PNG/MNG images"},
    {"da2b", "DMB MAF, extending da2a, with 3GPP timed text, DID, TVA, REL, IPMP"},
    {"da3a", "DMB MAF aud with HE-AAC aud, JPG/PNG/MNG images"},
    {"da3b", "DMB MAF, extending da3a w/ BIFS, 3GPP timed text, DID, TVA, REL, IPMP"},
    {"dmb1", "DMB MAF supporting all the components defined in the specification"},
    {"dmpf", "Digital Media Project"},
    {"drc1", "Dirac (wavelet compression), encapsulated in ISO base media (MP4)"},
    {"dv1a", "DMB MAF vid w/ AVC vid, ER-BSAC aud, BIFS, JPG/PNG/MNG images, TS"},
    {"dv1b", "DMB MAF, extending dv1a, with 3GPP timed text, DID, TVA, REL, IPMP"},
    {"dv2a", "DMB MAF vid w/ AVC vid, HE-AAC v2 aud, BIFS, JPG/PNG/MNG images, TS"},
    {"dv2b", "DMB MAF, extending dv2a, with 3GPP timed text, DID, TVA, REL, IPMP"},
    {"dv3a", "DMB MAF vid w/ AVC vid, HE-AAC aud, BIFS, JPG/PNG/MNG images, TS"},
    {"dv3b", "DMB MAF, extending dv3a, with 3GPP timed text, DID, TVA, REL, IPMP"},
    {"dvr1", "DVB (.DVB) over RTP"},
    {"dvt1", "DVB (.DVB) over MPEG-2 Transport Stream"},
    {"isc2", "ISMACryp 2.0 Encrypted File"},
    {"iso2", "MP4 Base Media v2 [ISO 14496-12:2005]"},
    {"isom", "MP4 Base Media v1 [IS0 14496-12:2003]"},
    {"jpm ", "JPEG 2000 Compound Image (.JPM) [ISO 15444-6]"},
    {"jpx ", "JPEG 2000 with extensions (.JPX) [ISO 15444-2]"},
    {"mj2s", "Motion JPEG 2000 [ISO 15444-3] Simple Profile"},
    {"mjp2", "Motion JPEG 2000 [ISO 15444-3] General Profile"},
    {"mmp4", "MPEG-4/3GPP Mobile Profile (.MP4/3GP) (for NTT)"},
    {"mp21", "MPEG-21 [ISO/IEC 21000-9]"},
    {"mp41", "MP4 v1 [ISO 14496-1:ch13]"},
    {"mp42", "MP4 v2 [ISO 14496-14]"},
    {"mp71", "MP4 w/ MPEG-7 Metadata [per ISO 14496-12]"},
    {"mqt ", "Sony / Mobile QuickTime (.MQV) US Patent 7,477,830 (Sony Corp)"},
    {"niko", "Nikon"},
    {"odcf", "OMA DCF DRM Format 2.0 (OMA-TS-DRM-DCF-V2_0-20060303-A)"},
    {"opf2", "OMA PDCF DRM Format 2.1 (OMA-TS-DRM-DCF-V2_1-20070724-C)"},
    {"opx2", "OMA PDCF DRM + XBS extensions (OMA-TS-DRM_XBS-V1_0-20070529-C)"},
    {"pana", "Panasonic Digital Camera"},
    {"qt  ", "Apple QuickTime (.MOV/QT)"},
    {"sdv ", "SD Memory Card Video"},
    {"ssc1", "Samsung stereoscopic, single stream"},
    {"ssc2", "Samsung stereoscopic, dual stream"},
};

static constexpr TagVocabulary handlerClassTags[] = {
    {"dhlr", "Data Handler"},
    {"mhlr", "Media Handler"},
};

static constexpr TagVocabulary handlerTypeTags[] = {
    {"alis", "Alias Data"},
    {"crsm", "Clock Reference"},
    {"hint", "Hint Track"},
    {"ipsm", "IPMP"},
    {"m7sm", "MPEG-7 Stream"},
    {"mdir", "Metadata"},
    {"mdta", "Metadata Tags"},
    {"mjsm", "MPEG-J"},
    {"ocsm", "Object Content"},
    {"odsm", "Object Descriptor"},
    {"sdsm", "Scene Description"},
    {"soun", "Audio Track"},
    {"text", "Text"},
    {"tmcd", "Time Code"},
    {"url ", "URL"},
    {"vide", "Video Track"},
};

static constexpr TagVocabulary vendorIDTags[] = {
    {"FFMP", "FFmpeg"},
    {"appl", "Apple"},
    {"olym", "Olympus"},
    {"GIC ", "General Imaging Co."},
    {"fe20", "Olympus (fe20)"},
    {"pana", "Panasonic"},
    {"KMPI", "Konica-Minolta"},
    {"kdak", "Kodak"},
    {"pent", "Pentax"},
    {"NIKO", "Nikon"},
    {"leic", "Leica"},
    {"pr01", "Olympus (pr01)"},
    {"SMI ", "Sorenson Media Inc."},
    {"mino", "Minolta"},
    {"sany", "Sanyo"},
    {"ZORA", "Zoran Corporation"},
    {"niko", "Nikon"},
};

static constexpr TagVocabulary cameraByteOrderTags[] = {
    {"II", "Little-endian (Intel, II)"},
    {"MM", "Big-endian (Motorola, MM)"},
};

static constexpr TagDetails graphicsModetags[] = {
    {0x0, "srcCopy"},
    {0x1, "srcOr"},
    {0x2, "srcXor"},
    {0x3, "srcBic"},
    {0x4, "notSrcCopy"},
    {0x5, "notSrcOr"},
    {0x6, "notSrcXor"},
    {0x7, "notSrcBic"},
    {0x8, "patCopy"},
    {0x9, "patOr"},
    {0xa, "patXor"},
    {0xb, "patBic"},
    {0xc, "notPatCopy"},
    {0xd, "notPatOr"},
    {0xe, "notPatXor"},
    {0xf, "notPatBic"},
    {0x20, "blend"},
    {0x21, "addPin"},
    {0x22, "addOver"},
    {0x23, "subPin"},
    {0x24, "transparent"},
    {0x25, "addMax"},
    {0x26, "subOver"},
    {0x27, "addMin"},
    {0x31, "grayishTextOr"},
    {0x32, "hilite"},
    {0x40, "ditherCopy"},
    {0x100, "Alpha"},
    {0x101, "White Alpha"},
    {0x102, "Pre-multiplied Black Alpha"},
    {0x110, "Component Alpha"},
};

static constexpr TagVocabulary userDatatags[] = {
    {"AllF", "PlayAllFrames"},
    {"CNCV", "CompressorVersion"},
    {"CNFV", "FirmwareVersion"},
    {"CNMN", "Model"},
    {"CNTH", "CanonCNTH"},
    {"DcMD", "DcMD"},
    {"FFMV", "FujiFilmFFMV"},
    {"INFO", "SamsungINFO"},
    {"LOOP", "LoopStyle"},
    {"MMA0", "MinoltaMMA0"},
    {"MMA1", "MinoltaMMA1"},
    {"MVTG", "FujiFilmMVTG"},
    {"NCDT", "NikonNCDT"},
    {"PANA", "PanasonicPANA"},
    {"PENT", "PentaxPENT"},
    {"PXMN", "MakerNotePentax5b"},
    {"PXTH", "PentaxPreview"},
    {"QVMI", "CasioQVMI"},
    {"SDLN", "PlayMode"},
    {"SelO", "PlaySelection"},
    {"TAGS", "KodakTags/KonicaMinoltaTags/MinoltaTags/NikonTags/OlympusTags/PentaxTags/SamsungTags/SanyoMOV/SanyoMP4"},
    {"WLOC", "WindowLocation"},
    {"XMP_", "XMP"},
    {"Xtra", "Xtra"},
    {"hinf", "HintTrackInfo"},
    {"hinv", "HintVersion"},
    {"hnti", "Hint"},
    {"meta", "Meta"},
    {"name", "Name"},
    {"ptv ", "PrintToVideo"},
    {"scrn", "OlympusPreview"},
    {"thmb", "MakerNotePentax5a/OlympusThumbnail"},
};

static constexpr TagVocabulary userDataReferencetags[] = {
    {"CNCV", "Xmp.video.CompressorVersion"},
    {"CNFV", "Xmp.video.FirmwareVersion"},
    {"CNMN", "Xmp.video.Model"},
    {"NCHD", "Xmp.video.MakerNoteType"},
    {"WLOC", "Xmp.video.WindowLocation"},
    {"SDLN", "Xmp.video.PlayMode"},
    {"FFMV", "Xmp.video.StreamName"},
    {"SelO", "Xmp.video.PlaySelection"},
    {"name", "Xmp.video.Name"},
    {"vndr", "Xmp.video.Vendor"},
    {" ART", "Xmp.video.Artist"},
    {" alb", "Xmp.video.Album"},
    {" arg", "Xmp.video.Arranger"},
    {" ark", "Xmp.video.ArrangerKeywords"},
    {" cmt", "Xmp.video.Comment"},
    {" cok", "Xmp.video.ComposerKeywords"},
    {" com", "Xmp.video.Composer"},
    {" cpy", "Xmp.video.Copyright"},
    {" day", "Xmp.video.CreateDate"},
    {" dir", "Xmp.video.Director"},
    {" ed1", "Xmp.video.Edit1"},
    {" ed2", "Xmp.video.Edit2"},
    {" ed3", "Xmp.video.Edit3"},
    {" ed4", "Xmp.video.Edit4"},
    {" ed5", "Xmp.video.Edit5"},
    {" ed6", "Xmp.video.Edit6"},
    {" ed7", "Xmp.video.Edit7"},
    {" ed8", "Xmp.video.Edit8"},
    {" ed9", "Xmp.video.Edit9"},
    {" enc", "Xmp.video.Encoder"},
    {" fmt", "Xmp.video.Format"},
    {" gen", "Xmp.video.Genre"},
    {" grp", "Xmp.video.Grouping"},
    {" inf", "Xmp.video.Information"},
    {" isr", "Xmp.video.ISRCCode"},
    {" lab", "Xmp.video.RecordLabelName"},
    {" lal", "Xmp.video.RecordLabelURL"},
    {" lyr", "Xmp.video.Lyrics"},
    {" mak", "Xmp.video.Make"},
    {" mal", "Xmp.video.MakerURL"},
    {" mod", "Xmp.video.Model"},
    {" nam", "Xmp.video.Title"},
    {" pdk", "Xmp.video.ProducerKeywords"},
    {" phg", "Xmp.video.RecordingCopyright"},
    {" prd", "Xmp.video.Producer"},
    {" prf", "Xmp.video.Performers"},
    {" prk", "Xmp.video.PerformerKeywords"},
    {" prl", "Xmp.video.PerformerURL"},
    {" req", "Xmp.video.Requirements"},
    {" snk", "Xmp.video.SubtitleKeywords"},
    {" snm", "Xmp.video.Subtitle"},
    {" src", "Xmp.video.SourceCredits"},
    {" swf", "Xmp.video.SongWriter"},
    {" swk", "Xmp.video.SongWriterKeywords"},
    {" swr", "Xmp.video.SoftwareVersion"},
    {" too", "Xmp.video.Encoder"},
    {" trk", "Xmp.video.Track"},
    {" wrt", "Xmp.video.Composer"},
    {" xyz", "Xmp.video.GPSCoordinates"},
    {"CMbo", "Xmp.video.CameraByteOrder"},
    {"Cmbo", "Xmp.video.CameraByteOrder"},
};

static constexpr TagDetails NikonNCTGTags[] = {
    {0x0001, "Xmp.video.Make"},
    {0x0002, "Xmp.video.Model"},
    {0x0003, "Xmp.video.Software"},
    {0x0011, "Xmp.video.CreationDate"},
    {0x0012, "Xmp.video.DateTimeOriginal"},
    {0x0013, "Xmp.video.FrameCount"},
    {0x0016, "Xmp.video.FrameRate"},
    {0x0022, "Xmp.video.FrameWidth"},
    {0x0023, "Xmp.video.FrameHeight"},
    {0x0032, "Xmp.audio.channelType"},
    {0x0033, "Xmp.audio.BitsPerSample"},
    {0x0034, "Xmp.audio.sampleRate"},
    {0x1108822, "Xmp.video.ExposureProgram"},
    {0x1109204, "Xmp.video.ExposureCompensation"},
    {0x1109207, "Xmp.video.MeteringMode"},
    {0x110a434, "Xmp.video.LensModel"},
    {0x1200000, "Xmp.video.GPSVersionID"},
    {0x1200001, "Xmp.video.GPSLatitudeRef"},
    {0x1200002, "Xmp.video.GPSLatitude"},
    {0x1200003, "Xmp.video.GPSLongitudeRef"},
    {0x1200004, "Xmp.video.GPSLongitude"},
    {0x1200005, "Xmp.video.GPSAltitudeRef"},
    {0x1200006, "Xmp.video.GPSAltitude"},
    {0x1200007, "Xmp.video.GPSTimeStamp"},
    {0x1200008, "Xmp.video.GPSSatellites"},
    {0x1200010, "Xmp.video.GPSImgDirectionRef"},
    {0x1200011, "Xmp.video.GPSImgDirection"},
    {0x1200012, "Xmp.video.GPSMapDatum"},
    {0x120001d, "Xmp.video.GPSDateStamp"},
    {0x2000001, "Xmp.video.MakerNoteVersion"},
    {0x2000005, "Xmp.video.WhiteBalance"},
    {0x200000b, "Xmp.video.WhiteBalanceFineTune"},
    {0x200001e, "Xmp.video.ColorSpace"},
    {0x2000023, "Xmp.video.PictureControlData"},
    {0x2000024, "Xmp.video.WorldTime"},
    {0x200002c, "Xmp.video.UnknownInfo"},
    {0x2000032, "Xmp.video.UnknownInfo2"},
    {0x2000039, "Xmp.video.LocationInfo"},
    {0x2000083, "Xmp.video.LensType"},
    {0x2000084, "Xmp.video.LensModel"},
    {0x20000ab, "Xmp.video.VariProgram"},
};

[[maybe_unused]] static constexpr TagDetails NikonColorSpace[] = {
    {1, "sRGB"},
    {2, "Adobe RGB"},
};

[[maybe_unused]] static constexpr TagVocabulary NikonGPS_Latitude_Longitude_ImgDirection_Reference[] = {
    {"N", "North"}, {"S", "South"}, {"E", "East"}, {"W", "West"}, {"M", "Magnetic North"}, {"T", "True North"},
};

[[maybe_unused]] static constexpr TagDetails NikonGPSAltitudeRef[] = {
    {0, "Above Sea Level"},
    {1, "Below Sea Level"},
};

[[maybe_unused]] static constexpr TagDetails NikonExposureProgram[] = {
    {0, "Not Defined"},
    {1, "Manual"},
    {2, "Program AE"},
    {3, "Aperture-priority AE"},
    {4, "Shutter speed priority AE"},
    {5, "Creative (Slow speed)"},
    {6, "Action (High speed)"},
    {7, "Portrait"},
    {8, "Landscape"},
};

[[maybe_unused]] static constexpr TagDetails NikonMeteringMode[] = {
    {0, "Unknown"}, {1, "Average"},    {2, "Center-weighted average"},
    {3, "Spot"},    {4, "Multi-spot"}, {5, "Multi-segment"},
    {6, "Partial"}, {255, "Other"},
};

static constexpr TagDetails PictureControlAdjust[] = {
    {0, "Default Settings"},
    {1, "Quick Adjust"},
    {2, "Full Control"},
};

//! Contrast and Sharpness
static constexpr TagDetails NormalSoftHard[] = {
    {0, "Normal"},
    {1, "Soft"},
    {2, "Hard"},
};

//! Saturation
static constexpr TagDetails Saturation[] = {
    {0, "Normal"},
    {1, "Low"},
    {2, "High"},
};

//! YesNo, used for DaylightSavings
static constexpr TagDetails YesNo[] = {
    {0, "No"},
    {1, "Yes"},
};

//! DateDisplayFormat
static constexpr TagDetails DateDisplayFormat[] = {
    {0, "Y/M/D"},
    {1, "M/D/Y"},
    {2, "D/M/Y"},
};

static constexpr TagDetails FilterEffect[] = {
    {0x80, "Off"}, {0x81, "Yellow"}, {0x82, "Orange"}, {0x83, "Red"}, {0x84, "Green"}, {0xff, "n/a"},
};

static constexpr TagDetails ToningEffect[] = {
    {0x80, "B&W"},         {0x81, "Sepia"},      {0x82, "Cyanotype"},  {0x83, "Red"},
    {0x84, "Yellow"},      {0x85, "Green"},      {0x86, "Blue-green"}, {0x87, "Blue"},
    {0x88, "Purple-blue"}, {0x89, "Red-purple"}, {0xff, "n/a"},
};

static constexpr TagDetails whiteBalance[] = {
    {0, "Auto"}, {1, "Daylight"}, {2, "Shade"}, {3, "Fluorescent"}, {4, "Tungsten"}, {5, "Manual"},
};

enum movieHeaderTags {
  MovieHeaderVersion,
  CreateDate,
  ModifyDate,
  TimeScale,
  Duration,
  PreferredRate,
  PreferredVolume,
  PreviewTime = 18,
  PreviewDuration,
  PosterTime,
  SelectionTime,
  SelectionDuration,
  CurrentTime,
  NextTrackID
};
enum trackHeaderTags {
  TrackHeaderVersion,
  TrackCreateDate,
  TrackModifyDate,
  TrackID,
  TrackDuration = 5,
  TrackLayer = 8,
  TrackVolume,
  ImageWidth = 19,
  ImageHeight
};
enum mediaHeaderTags {
  MediaHeaderVersion,
  MediaCreateDate,
  MediaModifyDate,
  MediaTimeScale,
  MediaDuration,
  MediaLanguageCode
};
enum handlerTags { HandlerClass = 1, HandlerType, HandlerVendorID };
enum videoHeaderTags { GraphicsMode = 2, OpColor };
enum stream { Video, Audio, Hint, Null, GenMediaHeader };
enum imageDescTags {
  codec,
  VendorID = 4,
  SourceImageWidth_Height = 7,
  XResolution,
  YResolution,
  CompressorName = 10,
  BitDepth
};
enum audioDescTags { AudioFormat, AudioVendorID = 4, AudioChannels, AudioSampleRate = 7, MOV_AudioFormat = 13 };

/*!
  @brief Function used to check equality of a Tags with a
      particular string (ignores case while comparing).
  @param buf Data buffer that will contain Tag to compare
  @param str char* Pointer to string
  @return Returns true if the buffer value is equal to string.
 */
static bool equalsQTimeTag(Exiv2::DataBuf& buf, const char* str) {
  for (int i = 0; i < 4; ++i)
    if (tolower(buf.data()[i]) != tolower(str[i]))
      return false;
  return true;
}

/*!
  @brief Function used to ignore Tags and values stored in them,
      since they are not necessary as metadata information
  @param buf Data buffer that will contain Tag to compare
  @return Returns true, if Tag is found in the ignoreList[]
 */
static bool ignoreList(Exiv2::DataBuf& buf) {
  const char ignoreList[13][5] = {
      "mdat", "edts", "junk", "iods", "alis", "stsc", "stsz", "stco", "ctts", "stss", "skip", "wide", "cmvd",
  };

  for (auto i : ignoreList)
    if (equalsQTimeTag(buf, i))
      return true;

  return false;
}

/*!
  @brief Function used to ignore Tags, basically Tags which
      contain other tags inside them, since they are not necessary
      as metadata information
  @param buf Data buffer that will contain Tag to compare
  @return Returns true, if Tag is found in the ignoreList[]
 */
static bool dataIgnoreList(Exiv2::DataBuf& buf) {
  const char ignoreList[8][5] = {
      "moov", "mdia", "minf", "dinf", "alis", "stbl", "cmov", "meta",
  };

  for (auto i : ignoreList)
    if (equalsQTimeTag(buf, i))
      return true;

  return false;
}
}  // namespace Exiv2::Internal

namespace Exiv2 {

using namespace Exiv2::Internal;

QuickTimeVideo::QuickTimeVideo(BasicIo::UniquePtr io) :
    Image(ImageType::qtime, mdNone, std::move(io)), timeScale_(1), currentStream_(Null) {
}  // QuickTimeVideo::QuickTimeVideo

std::string QuickTimeVideo::mimeType() const {
  return "video/quicktime";
}

void QuickTimeVideo::writeMetadata() {
}

void QuickTimeVideo::readMetadata() {
  if (io_->open() != 0)
    throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError());

  // Ensure that this is the correct image type
  if (!isQTimeType(*io_, false)) {
    if (io_->error() || io_->eof())
      throw Error(ErrorCode::kerFailedToReadImageData);
    throw Error(ErrorCode::kerNotAnImage, "QuickTime");
  }

  IoCloser closer(*io_);
  clearMetadata();
  continueTraversing_ = true;
  height_ = width_ = 1;

  xmpData_["Xmp.video.FileSize"] = static_cast<double>(io_->size()) / static_cast<double>(1048576);
  xmpData_["Xmp.video.MimeType"] = mimeType();

  while (continueTraversing_)
    decodeBlock();

  xmpData_["Xmp.video.AspectRatio"] = getAspectRatio(width_, height_);
}  // QuickTimeVideo::readMetadata

void QuickTimeVideo::decodeBlock(std::string const& entered_from) {
  const long bufMinSize = 4;
  DataBuf buf(bufMinSize + 1);
  uint64_t size = 0;
  buf.data()[4] = '\0';

  io_->read(buf.data(), 4);
  if (io_->eof()) {
    continueTraversing_ = false;
    return;
  }

  size = buf.read_uint32(0, bigEndian);

  io_->readOrThrow(buf.data(), 4);

  // we have read 2x 4 bytes
  size_t hdrsize = 8;

  if (size == 1) {
    // The box size is encoded as a uint64_t, so we need to read another 8 bytes.
    DataBuf data(8);
    hdrsize += 8;
    io_->readOrThrow(data.data(), data.size());
    size = data.read_uint64(0, bigEndian);
  } else if (size == 0 && entered_from == "meta") {
    size = buf.read_uint32(0, bigEndian);
    io_->readOrThrow(buf.data(), 4, Exiv2::ErrorCode::kerCorruptedMetadata);
  }

  enforce(size >= hdrsize, Exiv2::ErrorCode::kerCorruptedMetadata);
  enforce(size - hdrsize <= io_->size() - io_->tell(), Exiv2::ErrorCode::kerCorruptedMetadata);
  enforce(size - hdrsize <= std::numeric_limits<size_t>::max(), Exiv2::ErrorCode::kerCorruptedMetadata);

  // std::cerr<<"Tag=>"<<buf.data()<<"     size=>"<<size-hdrsize << std::endl;
  const auto newsize = static_cast<size_t>(size - hdrsize);
  if (newsize > buf.size()) {
    buf.resize(newsize);
  }
  tagDecoder(buf, newsize);
}  // QuickTimeVideo::decodeBlock

static std::string readString(BasicIo& io, size_t size) {
  enforce(size <= io.size() - io.tell(), Exiv2::ErrorCode::kerCorruptedMetadata);
  Exiv2::DataBuf str(size + 1);
  io.readOrThrow(str.data(), size);
  str.write_uint8(size, 0);  // nul-terminate string
  return Exiv2::toString(str.data());
}

void QuickTimeVideo::tagDecoder(Exiv2::DataBuf& buf, size_t size) {
  assert(buf.size() > 4);

  if (ignoreList(buf))
    discard(size);

  else if (dataIgnoreList(buf)) {
    decodeBlock(Exiv2::toString(buf.data()));
  } else if (equalsQTimeTag(buf, "ftyp"))
    fileTypeDecoder(size);

  else if (equalsQTimeTag(buf, "trak"))
    setMediaStream();

  else if (equalsQTimeTag(buf, "mvhd"))
    movieHeaderDecoder(size);

  else if (equalsQTimeTag(buf, "tkhd"))
    trackHeaderDecoder(size);

  else if (equalsQTimeTag(buf, "mdhd"))
    mediaHeaderDecoder(size);

  else if (equalsQTimeTag(buf, "hdlr"))
    handlerDecoder(size);

  else if (equalsQTimeTag(buf, "vmhd"))
    videoHeaderDecoder(size);

  else if (equalsQTimeTag(buf, "udta"))
    userDataDecoder(size);

  else if (equalsQTimeTag(buf, "dref"))
    multipleEntriesDecoder();

  else if (equalsQTimeTag(buf, "stsd"))
    sampleDesc(size);

  else if (equalsQTimeTag(buf, "stts"))
    timeToSampleDecoder();

  else if (equalsQTimeTag(buf, "pnot"))
    previewTagDecoder(size);

  else if (equalsQTimeTag(buf, "tapt"))
    trackApertureTagDecoder(size);

  else if (equalsQTimeTag(buf, "keys"))
    keysTagDecoder(size);

  else if (equalsQTimeTag(buf, "url ")) {
    if (currentStream_ == Video)
      xmpData_["Xmp.video.URL"] = readString(*io_, size);
    else if (currentStream_ == Audio)
      xmpData_["Xmp.audio.URL"] = readString(*io_, size);
  }

  else if (equalsQTimeTag(buf, "urn ")) {
    if (currentStream_ == Video)
      xmpData_["Xmp.video.URN"] = readString(*io_, size);
    else if (currentStream_ == Audio)
      xmpData_["Xmp.audio.URN"] = readString(*io_, size);
  }

  else if (equalsQTimeTag(buf, "dcom")) {
    xmpData_["Xmp.video.Compressor"] = readString(*io_, size);
  }

  else if (equalsQTimeTag(buf, "smhd")) {
    io_->readOrThrow(buf.data(), 4);
    io_->readOrThrow(buf.data(), 4);
    xmpData_["Xmp.audio.Balance"] = buf.read_uint16(0, bigEndian);
  }

  else {
    discard(size);
  }
}  // QuickTimeVideo::tagDecoder

void QuickTimeVideo::discard(size_t size) {
  size_t cur_pos = io_->tell();
  io_->seek(cur_pos + size, BasicIo::beg);
}  // QuickTimeVideo::discard

void QuickTimeVideo::previewTagDecoder(size_t size) {
  DataBuf buf(4);
  size_t cur_pos = io_->tell();
  io_->readOrThrow(buf.data(), 4);
  xmpData_["Xmp.video.PreviewDate"] = buf.read_uint32(0, bigEndian);
  io_->readOrThrow(buf.data(), 2);
  xmpData_["Xmp.video.PreviewVersion"] = getShort(buf.data(), bigEndian);

  io_->readOrThrow(buf.data(), 4);
  if (equalsQTimeTag(buf, "PICT"))
    xmpData_["Xmp.video.PreviewAtomType"] = "QuickDraw Picture";
  else
    xmpData_["Xmp.video.PreviewAtomType"] = std::string{buf.c_str(), 4};

  io_->seek(cur_pos + size, BasicIo::beg);
}  // QuickTimeVideo::previewTagDecoder

void QuickTimeVideo::keysTagDecoder(size_t size) {
  DataBuf buf(4);
  size_t cur_pos = io_->tell();
  io_->readOrThrow(buf.data(), 4);
  xmpData_["Xmp.video.PreviewDate"] = buf.read_uint32(0, bigEndian);
  io_->readOrThrow(buf.data(), 2);
  xmpData_["Xmp.video.PreviewVersion"] = getShort(buf.data(), bigEndian);

  io_->readOrThrow(buf.data(), 4);
  if (equalsQTimeTag(buf, "PICT"))
    xmpData_["Xmp.video.PreviewAtomType"] = "QuickDraw Picture";
  else
    xmpData_["Xmp.video.PreviewAtomType"] = std::string{buf.c_str(), 4};

  io_->seek(cur_pos + size, BasicIo::beg);
}  // QuickTimeVideo::keysTagDecoder

void QuickTimeVideo::trackApertureTagDecoder(size_t size) {
  DataBuf buf(4);
  DataBuf buf2(2);
  size_t cur_pos = io_->tell();
  byte n = 3;

  while (n--) {
    io_->seek(static_cast<long>(4), BasicIo::cur);
    io_->readOrThrow(buf.data(), 4);

    if (equalsQTimeTag(buf, "clef")) {
      io_->seek(static_cast<long>(4), BasicIo::cur);
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      xmpData_["Xmp.video.CleanApertureWidth"] =
          Exiv2::toString(buf.read_uint16(0, bigEndian)) + "." + Exiv2::toString(buf2.read_uint16(0, bigEndian));
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      xmpData_["Xmp.video.CleanApertureHeight"] =
          Exiv2::toString(buf.read_uint16(0, bigEndian)) + "." + Exiv2::toString(buf2.read_uint16(0, bigEndian));
    }

    else if (equalsQTimeTag(buf, "prof")) {
      io_->seek(static_cast<long>(4), BasicIo::cur);
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      xmpData_["Xmp.video.ProductionApertureWidth"] =
          Exiv2::toString(buf.read_uint16(0, bigEndian)) + "." + Exiv2::toString(buf2.read_uint16(0, bigEndian));
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      xmpData_["Xmp.video.ProductionApertureHeight"] =
          Exiv2::toString(buf.read_uint16(0, bigEndian)) + "." + Exiv2::toString(buf2.read_uint16(0, bigEndian));
    }

    else if (equalsQTimeTag(buf, "enof")) {
      io_->seek(static_cast<long>(4), BasicIo::cur);
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      xmpData_["Xmp.video.EncodedPixelsWidth"] =
          Exiv2::toString(buf.read_uint16(0, bigEndian)) + "." + Exiv2::toString(buf2.read_uint16(0, bigEndian));
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      xmpData_["Xmp.video.EncodedPixelsHeight"] =
          Exiv2::toString(buf.read_uint16(0, bigEndian)) + "." + Exiv2::toString(buf2.read_uint16(0, bigEndian));
    }
  }
  io_->seek(static_cast<long>(cur_pos + size), BasicIo::beg);
}  // QuickTimeVideo::trackApertureTagDecoder

void QuickTimeVideo::CameraTagsDecoder(size_t size_external) {
  size_t cur_pos = io_->tell();
  DataBuf buf(50);
  DataBuf buf2(4);
  const TagDetails* td;

  io_->readOrThrow(buf.data(), 4);
  if (equalsQTimeTag(buf, "NIKO")) {
    io_->seek(cur_pos, BasicIo::beg);

    io_->readOrThrow(buf.data(), 24);
    xmpData_["Xmp.video.Make"] = Exiv2::toString(buf.data());
    io_->readOrThrow(buf.data(), 14);
    xmpData_["Xmp.video.Model"] = Exiv2::toString(buf.data());
    io_->readOrThrow(buf.data(), 4);
    xmpData_["Xmp.video.ExposureTime"] =
        "1/" + Exiv2::toString(ceil(buf.read_uint32(0, littleEndian) / static_cast<double>(10)));
    io_->readOrThrow(buf.data(), 4);
    io_->readOrThrow(buf2.data(), 4);
    xmpData_["Xmp.video.FNumber"] =
        buf.read_uint32(0, littleEndian) / static_cast<double>(buf2.read_uint32(0, littleEndian));
    io_->readOrThrow(buf.data(), 4);
    io_->readOrThrow(buf2.data(), 4);
    xmpData_["Xmp.video.ExposureCompensation"] =
        buf.read_uint32(0, littleEndian) / static_cast<double>(buf2.read_uint32(0, littleEndian));
    io_->readOrThrow(buf.data(), 10);
    io_->readOrThrow(buf.data(), 4);
    td = Exiv2::find(whiteBalance, buf.read_uint32(0, littleEndian));
    if (td)
      xmpData_["Xmp.video.WhiteBalance"] = exvGettext(td->label_);
    io_->readOrThrow(buf.data(), 4);
    io_->readOrThrow(buf2.data(), 4);
    xmpData_["Xmp.video.FocalLength"] =
        buf.read_uint32(0, littleEndian) / static_cast<double>(buf2.read_uint32(0, littleEndian));
    io_->seek(static_cast<long>(95), BasicIo::cur);
    io_->readOrThrow(buf.data(), 48);
    buf.write_uint8(48, 0);
    xmpData_["Xmp.video.Software"] = Exiv2::toString(buf.data());
    io_->readOrThrow(buf.data(), 4);
    xmpData_["Xmp.video.ISO"] = buf.read_uint32(0, littleEndian);
  }

  io_->seek(cur_pos + size_external, BasicIo::beg);
}  // QuickTimeVideo::CameraTagsDecoder

void QuickTimeVideo::userDataDecoder(size_t size_external) {
  size_t cur_pos = io_->tell();
  const TagVocabulary* td;
  const TagVocabulary* tv;
  const TagVocabulary* tv_internal;

  const long bufMinSize = 100;
  DataBuf buf(bufMinSize);
  size_t size_internal = size_external;
  std::memset(buf.data(), 0x0, buf.size());

  while ((size_internal / 4 != 0) && (size_internal > 0)) {
    buf.data()[4] = '\0';
    io_->readOrThrow(buf.data(), 4);
    const size_t size = buf.read_uint32(0, bigEndian);
    if (size > size_internal)
      break;
    size_internal -= size;
    io_->readOrThrow(buf.data(), 4);

    if (buf.data()[0] == 169)
      buf.data()[0] = ' ';
    td = Exiv2::find(userDatatags, Exiv2::toString(buf.data()));

    tv = Exiv2::find(userDataReferencetags, Exiv2::toString(buf.data()));

    if (size <= 12)
      break;

    if (equalsQTimeTag(buf, "DcMD") || equalsQTimeTag(buf, "NCDT"))
      userDataDecoder(size - 8);

    else if (equalsQTimeTag(buf, "NCTG"))
      NikonTagsDecoder(size - 8);

    else if (equalsQTimeTag(buf, "TAGS"))
      CameraTagsDecoder(size - 8);

    else if (equalsQTimeTag(buf, "CNCV") || equalsQTimeTag(buf, "CNFV") || equalsQTimeTag(buf, "CNMN") ||
             equalsQTimeTag(buf, "NCHD") || equalsQTimeTag(buf, "FFMV")) {
      enforce(tv, Exiv2::ErrorCode::kerCorruptedMetadata);
      xmpData_[exvGettext(tv->label_)] = readString(*io_, size - 8);
    }

    else if (equalsQTimeTag(buf, "CMbo") || equalsQTimeTag(buf, "Cmbo")) {
      enforce(tv, Exiv2::ErrorCode::kerCorruptedMetadata);
      io_->readOrThrow(buf.data(), 2);
      buf.data()[2] = '\0';
      tv_internal = Exiv2::find(cameraByteOrderTags, Exiv2::toString(buf.data()));

      if (tv_internal)
        xmpData_[exvGettext(tv->label_)] = exvGettext(tv_internal->label_);
      else
        xmpData_[exvGettext(tv->label_)] = Exiv2::toString(buf.data());
    }

    else if (tv) {
      io_->readOrThrow(buf.data(), 4);
      xmpData_[exvGettext(tv->label_)] = readString(*io_, size - 12);
    }

    else if (td)
      tagDecoder(buf, size - 8);
  }

  io_->seek(cur_pos + size_external, BasicIo::beg);
}  // QuickTimeVideo::userDataDecoder

void QuickTimeVideo::NikonTagsDecoder(size_t size_external) {
  size_t cur_pos = io_->tell();
  DataBuf buf(200);
  DataBuf buf2(4 + 1);
  uint32_t TagID = 0;
  uint16_t dataLength = 0;
  uint16_t dataType = 2;
  const TagDetails* td;
  const TagDetails* td2;

  for (int i = 0; i < 100; i++) {
    io_->readOrThrow(buf.data(), 4);
    TagID = buf.read_uint32(0, bigEndian);
    td = Exiv2::find(NikonNCTGTags, TagID);

    io_->readOrThrow(buf.data(), 2);
    dataType = buf.read_uint16(0, bigEndian);

    std::memset(buf.data(), 0x0, buf.size());
    io_->readOrThrow(buf.data(), 2);

    if (TagID == 0x2000023) {
      size_t local_pos = io_->tell();
      dataLength = buf.read_uint16(0, bigEndian);
      std::memset(buf.data(), 0x0, buf.size());

      io_->readOrThrow(buf.data(), 4);
      xmpData_["Xmp.video.PictureControlVersion"] = Exiv2::toString(buf.data());
      io_->readOrThrow(buf.data(), 20);
      xmpData_["Xmp.video.PictureControlName"] = Exiv2::toString(buf.data());
      io_->readOrThrow(buf.data(), 20);
      xmpData_["Xmp.video.PictureControlBase"] = Exiv2::toString(buf.data());
      io_->readOrThrow(buf.data(), 4);
      std::memset(buf.data(), 0x0, buf.size());

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(PictureControlAdjust, static_cast<int>(buf.data()[0]) & 7);
      if (td2)
        xmpData_["Xmp.video.PictureControlAdjust"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.PictureControlAdjust"] = static_cast<int>(buf.data()[0]) & 7;

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(NormalSoftHard, static_cast<int>(buf.data()[0]) & 7);
      if (td2)
        xmpData_["Xmp.video.PictureControlQuickAdjust"] = exvGettext(td2->label_);

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(NormalSoftHard, static_cast<int>(buf.data()[0]) & 7);
      if (td2)
        xmpData_["Xmp.video.Sharpness"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.Sharpness"] = static_cast<int>(buf.data()[0]) & 7;

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(NormalSoftHard, static_cast<int>(buf.data()[0]) & 7);
      if (td2)
        xmpData_["Xmp.video.Contrast"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.Contrast"] = static_cast<int>(buf.data()[0]) & 7;

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(NormalSoftHard, static_cast<int>(buf.data()[0]) & 7);
      if (td2)
        xmpData_["Xmp.video.Brightness"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.Brightness"] = static_cast<int>(buf.data()[0]) & 7;

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(Saturation, static_cast<int>(buf.data()[0]) & 7);
      if (td2)
        xmpData_["Xmp.video.Saturation"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.Saturation"] = static_cast<int>(buf.data()[0]) & 7;

      io_->readOrThrow(buf.data(), 1);
      xmpData_["Xmp.video.HueAdjustment"] = static_cast<int>(buf.data()[0]) & 7;

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(FilterEffect, static_cast<int>(buf.data()[0]));
      if (td2)
        xmpData_["Xmp.video.FilterEffect"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.FilterEffect"] = static_cast<int>(buf.data()[0]);

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(ToningEffect, static_cast<int>(buf.data()[0]));
      if (td2)
        xmpData_["Xmp.video.ToningEffect"] = exvGettext(td2->label_);
      else
        xmpData_["Xmp.video.ToningEffect"] = static_cast<int>(buf.data()[0]);

      io_->readOrThrow(buf.data(), 1);
      xmpData_["Xmp.video.ToningSaturation"] = static_cast<int>(buf.data()[0]);

      io_->seek(local_pos + dataLength, BasicIo::beg);
    }

    else if (TagID == 0x2000024) {
      size_t local_pos = io_->tell();
      dataLength = buf.read_uint16(0, bigEndian);
      std::memset(buf.data(), 0x0, buf.size());

      io_->readOrThrow(buf.data(), 2);
      xmpData_["Xmp.video.TimeZone"] = Exiv2::getShort(buf.data(), bigEndian);
      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(YesNo, static_cast<int>(buf.data()[0]));
      if (td2)
        xmpData_["Xmp.video.DayLightSavings"] = exvGettext(td2->label_);

      io_->readOrThrow(buf.data(), 1);
      td2 = Exiv2::find(DateDisplayFormat, static_cast<int>(buf.data()[0]));
      if (td2)
        xmpData_["Xmp.video.DateDisplayFormat"] = exvGettext(td2->label_);

      io_->seek(local_pos + dataLength, BasicIo::beg);
    }

    else if (dataType == 2 || dataType == 7) {
      dataLength = buf.read_uint16(0, bigEndian);
      std::memset(buf.data(), 0x0, buf.size());

      // Sanity check with an "unreasonably" large number
      if (dataLength > 200) {
#ifndef SUPPRESS_WARNINGS
        EXV_ERROR << "Xmp.video Nikon Tags, dataLength was found to be larger than 200."
                  << " Entries considered invalid. Not Processed.\n";
#endif
        io_->seek(io_->tell() + dataLength, BasicIo::beg);
      } else {
        io_->readOrThrow(buf.data(), dataLength);
      }

      if (td) {
        xmpData_[exvGettext(td->label_)] = Exiv2::toString(buf.data());
      }
    } else if (dataType == 4) {
      dataLength = buf.read_uint16(0, bigEndian) * 4;
      std::memset(buf.data(), 0x0, buf.size());
      io_->readOrThrow(buf.data(), 4);
      if (td)
        xmpData_[exvGettext(td->label_)] = Exiv2::toString(buf.read_uint32(0, bigEndian));

      // Sanity check with an "unreasonably" large number
      if (dataLength > 200 || dataLength < 4) {
#ifndef SUPPRESS_WARNINGS
        EXV_ERROR << "Xmp.video Nikon Tags, dataLength was found to be of inappropriate size."
                  << " Entries considered invalid. Not Processed.\n";
#endif
        io_->seek(io_->tell() + dataLength - 4, BasicIo::beg);
      } else
        io_->readOrThrow(buf.data(), dataLength - 4);
    } else if (dataType == 3) {
      dataLength = buf.read_uint16(0, bigEndian) * 2;
      std::memset(buf.data(), 0x0, buf.size());
      io_->readOrThrow(buf.data(), 2);
      if (td)
        xmpData_[exvGettext(td->label_)] = Exiv2::toString(buf.read_uint16(0, bigEndian));

      // Sanity check with an "unreasonably" large number
      if (dataLength > 200 || dataLength < 2) {
#ifndef SUPPRESS_WARNINGS
        EXV_ERROR << "Xmp.video Nikon Tags, dataLength was found to be of inappropriate size."
                  << " Entries considered invalid. Not Processed.\n";
#endif
        io_->seek(io_->tell() + dataLength - 2, BasicIo::beg);
      } else
        io_->readOrThrow(buf.data(), dataLength - 2);
    } else if (dataType == 5) {
      dataLength = buf.read_uint16(0, bigEndian) * 8;
      std::memset(buf.data(), 0x0, buf.size());
      io_->readOrThrow(buf.data(), 4);
      io_->readOrThrow(buf2.data(), 4);
      if (td)
        xmpData_[exvGettext(td->label_)] = Exiv2::toString(static_cast<double>(buf.read_uint32(0, bigEndian)) /
                                                           static_cast<double>(buf2.read_uint32(0, bigEndian)));

      // Sanity check with an "unreasonably" large number
      if (dataLength > 200 || dataLength < 8) {
#ifndef SUPPRESS_WARNINGS
        EXV_ERROR << "Xmp.video Nikon Tags, dataLength was found to be of inappropriate size."
                  << " Entries considered invalid. Not Processed.\n";
#endif
        io_->seek(io_->tell() + dataLength - 8, BasicIo::beg);
      } else
        io_->readOrThrow(buf.data(), dataLength - 8);
    } else if (dataType == 8) {
      dataLength = buf.read_uint16(0, bigEndian) * 2;
      std::memset(buf.data(), 0x0, buf.size());
      io_->readOrThrow(buf.data(), 2);
      io_->readOrThrow(buf2.data(), 2);
      if (td)
        xmpData_[exvGettext(td->label_)] =
            Exiv2::toString(buf.read_uint16(0, bigEndian)) + " " + Exiv2::toString(buf2.read_uint16(0, bigEndian));

      // Sanity check with an "unreasonably" large number
      if (dataLength > 200 || dataLength < 4) {
#ifndef SUPPRESS_WARNINGS
        EXV_ERROR << "Xmp.video Nikon Tags, dataLength was found to be of inappropriate size."
                  << " Entries considered invalid. Not Processed.\n";
#endif
        io_->seek(io_->tell() + dataLength - 4, BasicIo::beg);
      } else
        io_->readOrThrow(buf.data(), dataLength - 4);
    }
  }

  io_->seek(cur_pos + size_external, BasicIo::beg);
}  // QuickTimeVideo::NikonTagsDecoder

void QuickTimeVideo::setMediaStream() {
  size_t current_position = io_->tell();
  DataBuf buf(4 + 1);

  while (!io_->eof()) {
    io_->readOrThrow(buf.data(), 4);
    if (equalsQTimeTag(buf, "hdlr")) {
      io_->readOrThrow(buf.data(), 4);
      io_->readOrThrow(buf.data(), 4);
      io_->readOrThrow(buf.data(), 4);

      if (equalsQTimeTag(buf, "vide"))
        currentStream_ = Video;
      else if (equalsQTimeTag(buf, "soun"))
        currentStream_ = Audio;
      else if (equalsQTimeTag(buf, "hint"))
        currentStream_ = Hint;
      else
        currentStream_ = GenMediaHeader;
      break;
    }
  }

  io_->seek(current_position, BasicIo::beg);
}  // QuickTimeVideo::setMediaStream

void QuickTimeVideo::timeToSampleDecoder() {
  DataBuf buf(4 + 1);
  io_->readOrThrow(buf.data(), 4);
  io_->readOrThrow(buf.data(), 4);
  uint64_t totalframes = 0;
  uint64_t timeOfFrames = 0;
  const uint32_t noOfEntries = buf.read_uint32(0, bigEndian);

  for (uint32_t i = 0; i < noOfEntries; i++) {
    io_->readOrThrow(buf.data(), 4);
    const uint64_t temp = buf.read_uint32(0, bigEndian);
    totalframes = Safe::add(totalframes, temp);
    io_->readOrThrow(buf.data(), 4);
    timeOfFrames = Safe::add(timeOfFrames, temp * buf.read_uint32(0, bigEndian));
  }
  if (currentStream_ == Video)
    xmpData_["Xmp.video.FrameRate"] =
        static_cast<double>(totalframes) * static_cast<double>(timeScale_) / static_cast<double>(timeOfFrames);
}  // QuickTimeVideo::timeToSampleDecoder

void QuickTimeVideo::sampleDesc(size_t size) {
  DataBuf buf(100);
  size_t cur_pos = io_->tell();
  io_->readOrThrow(buf.data(), 4);
  io_->readOrThrow(buf.data(), 4);
  const uint32_t noOfEntries = buf.read_uint32(0, bigEndian);

  for (uint32_t i = 0; i < noOfEntries; i++) {
    if (currentStream_ == Video)
      imageDescDecoder();
    else if (currentStream_ == Audio)
      audioDescDecoder();
    else
      break;
  }
  io_->seek(Safe::add(cur_pos, size), BasicIo::beg);
}  // QuickTimeVideo::sampleDesc

void QuickTimeVideo::audioDescDecoder() {
  DataBuf buf(40);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';
  io_->readOrThrow(buf.data(), 4);
  size_t size = 82;

  const TagVocabulary* td;

  for (int i = 0; size / 4 != 0; size -= 4, i++) {
    io_->readOrThrow(buf.data(), 4);
    switch (i) {
      case AudioFormat:
        td = Exiv2::find(qTimeFileType, Exiv2::toString(buf.data()));
        if (td)
          xmpData_["Xmp.audio.Compressor"] = exvGettext(td->label_);
        else
          xmpData_["Xmp.audio.Compressor"] = Exiv2::toString(buf.data());
        break;
      case AudioVendorID:
        td = Exiv2::find(vendorIDTags, Exiv2::toString(buf.data()));
        if (td)
          xmpData_["Xmp.audio.VendorID"] = exvGettext(td->label_);
        break;
      case AudioChannels:
        xmpData_["Xmp.audio.ChannelType"] = buf.read_uint16(0, bigEndian);
        xmpData_["Xmp.audio.BitsPerSample"] = (buf.data()[2] * 256 + buf.data()[3]);
        break;
      case AudioSampleRate:
        xmpData_["Xmp.audio.SampleRate"] =
            buf.read_uint16(0, bigEndian) + ((buf.data()[2] * 256 + buf.data()[3]) * 0.01);
        break;
      default:
        break;
    }
  }
  io_->readOrThrow(buf.data(), static_cast<long>(size % 4));  // cause size is so small, this cast should be right.
}  // QuickTimeVideo::audioDescDecoder

void QuickTimeVideo::imageDescDecoder() {
  DataBuf buf(40);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';
  io_->readOrThrow(buf.data(), 4);
  size_t size = 82;

  const TagVocabulary* td;

  for (int i = 0; size / 4 != 0; size -= 4, i++) {
    io_->readOrThrow(buf.data(), 4);

    switch (i) {
      case codec:
        td = Exiv2::find(qTimeFileType, Exiv2::toString(buf.data()));
        if (td)
          xmpData_["Xmp.video.Codec"] = exvGettext(td->label_);
        else
          xmpData_["Xmp.video.Codec"] = Exiv2::toString(buf.data());
        break;
      case VendorID:
        td = Exiv2::find(vendorIDTags, Exiv2::toString(buf.data()));
        if (td)
          xmpData_["Xmp.video.VendorID"] = exvGettext(td->label_);
        break;
      case SourceImageWidth_Height:
        xmpData_["Xmp.video.SourceImageWidth"] = buf.read_uint16(0, bigEndian);
        xmpData_["Xmp.video.SourceImageHeight"] = (buf.data()[2] * 256 + buf.data()[3]);
        break;
      case XResolution:
        xmpData_["Xmp.video.XResolution"] =
            buf.read_uint16(0, bigEndian) + ((buf.data()[2] * 256 + buf.data()[3]) * 0.01);
        break;
      case YResolution:
        xmpData_["Xmp.video.YResolution"] =
            buf.read_uint16(0, bigEndian) + ((buf.data()[2] * 256 + buf.data()[3]) * 0.01);
        io_->readOrThrow(buf.data(), 3);
        size -= 3;
        break;
      case CompressorName:
        io_->readOrThrow(buf.data(), 32);
        size -= 32;
        xmpData_["Xmp.video.Compressor"] = Exiv2::toString(buf.data());
        break;
      default:
        break;
    }
  }
  io_->readOrThrow(buf.data(), static_cast<long>(size % 4));
  xmpData_["Xmp.video.BitDepth"] = static_cast<int>(buf.read_uint8(0));
}  // QuickTimeVideo::imageDescDecoder

void QuickTimeVideo::multipleEntriesDecoder() {
  DataBuf buf(4 + 1);
  io_->readOrThrow(buf.data(), 4);
  io_->readOrThrow(buf.data(), 4);
  uint32_t noOfEntries;

  noOfEntries = buf.read_uint32(0, bigEndian);

  for (uint32_t i = 0; i < noOfEntries && continueTraversing_; i++) {
    decodeBlock();
  }
}  // QuickTimeVideo::multipleEntriesDecoder

void QuickTimeVideo::videoHeaderDecoder(size_t size) {
  DataBuf buf(3);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[2] = '\0';
  currentStream_ = Video;

  const TagDetails* td;

  for (int i = 0; size / 2 != 0; size -= 2, i++) {
    io_->readOrThrow(buf.data(), 2);

    switch (i) {
      case GraphicsMode:
        td = Exiv2::find(graphicsModetags, buf.read_uint16(0, bigEndian));
        if (td)
          xmpData_["Xmp.video.GraphicsMode"] = exvGettext(td->label_);
        break;
      case OpColor:
        xmpData_["Xmp.video.OpColor"] = buf.read_uint16(0, bigEndian);
        break;
      default:
        break;
    }
  }
  io_->readOrThrow(buf.data(), size % 2);
}  // QuickTimeVideo::videoHeaderDecoder

void QuickTimeVideo::handlerDecoder(size_t size) {
  size_t cur_pos = io_->tell();
  DataBuf buf(100);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';

  const TagVocabulary* tv;

  for (int i = 0; i < 5; i++) {
    io_->readOrThrow(buf.data(), 4);

    switch (i) {
      case HandlerClass:
        tv = Exiv2::find(handlerClassTags, Exiv2::toString(buf.data()));
        if (tv) {
          if (currentStream_ == Video)
            xmpData_["Xmp.video.HandlerClass"] = exvGettext(tv->label_);
          else if (currentStream_ == Audio)
            xmpData_["Xmp.audio.HandlerClass"] = exvGettext(tv->label_);
        }
        break;
      case HandlerType:
        tv = Exiv2::find(handlerTypeTags, Exiv2::toString(buf.data()));
        if (tv) {
          if (currentStream_ == Video)
            xmpData_["Xmp.video.HandlerType"] = exvGettext(tv->label_);
          else if (currentStream_ == Audio)
            xmpData_["Xmp.audio.HandlerType"] = exvGettext(tv->label_);
        }
        break;
      case HandlerVendorID:
        tv = Exiv2::find(vendorIDTags, Exiv2::toString(buf.data()));
        if (tv) {
          if (currentStream_ == Video)
            xmpData_["Xmp.video.HandlerVendorID"] = exvGettext(tv->label_);
          else if (currentStream_ == Audio)
            xmpData_["Xmp.audio.HandlerVendorID"] = exvGettext(tv->label_);
        }
        break;
    }
  }
  io_->seek(cur_pos + size, BasicIo::beg);
}  // QuickTimeVideo::handlerDecoder

void QuickTimeVideo::fileTypeDecoder(size_t size) {
  DataBuf buf(5);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';
  Exiv2::Value::UniquePtr v = Exiv2::Value::create(Exiv2::xmpSeq);
  const TagVocabulary* td;

  for (int i = 0; size / 4 != 0; size -= 4, i++) {
    io_->readOrThrow(buf.data(), 4);
    td = Exiv2::find(qTimeFileType, Exiv2::toString(buf.data()));

    switch (i) {
      case 0:
        if (td)
          xmpData_["Xmp.video.MajorBrand"] = exvGettext(td->label_);
        break;
      case 1:
        xmpData_["Xmp.video.MinorVersion"] = buf.read_uint32(0, bigEndian);
        break;
      default:
        if (td)
          v->read(exvGettext(td->label_));
        else
          v->read(Exiv2::toString(buf.data()));
        break;
    }
  }
  xmpData_.add(Exiv2::XmpKey("Xmp.video.CompatibleBrands"), v.get());
  io_->readOrThrow(buf.data(), size % 4);
}  // QuickTimeVideo::fileTypeDecoder

void QuickTimeVideo::mediaHeaderDecoder(size_t size) {
  DataBuf buf(5);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';
  int64_t time_scale = 1;

  for (int i = 0; size / 4 != 0; size -= 4, i++) {
    io_->readOrThrow(buf.data(), 4);

    switch (i) {
      case MediaHeaderVersion:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.MediaHeaderVersion"] = static_cast<int>(buf.read_uint8(0));
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.MediaHeaderVersion"] = static_cast<int>(buf.read_uint8(0));
        break;
      case MediaCreateDate:
        // A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the movie atom was created.
        if (currentStream_ == Video)
          xmpData_["Xmp.video.MediaCreateDate"] = buf.read_uint32(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.MediaCreateDate"] = buf.read_uint32(0, bigEndian);
        break;
      case MediaModifyDate:
        // A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the movie atom was created.
        if (currentStream_ == Video)
          xmpData_["Xmp.video.MediaModifyDate"] = buf.read_uint32(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.MediaModifyDate"] = buf.read_uint32(0, bigEndian);
        break;
      case MediaTimeScale:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.MediaTimeScale"] = buf.read_uint32(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.MediaTimeScale"] = buf.read_uint32(0, bigEndian);
        time_scale = buf.read_uint32(0, bigEndian);
        if (time_scale <= 0)
          time_scale = 1;
        break;
      case MediaDuration:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.MediaDuration"] = time_scale ? buf.read_uint32(0, bigEndian) / time_scale : 0;
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.MediaDuration"] = time_scale ? buf.read_uint32(0, bigEndian) / time_scale : 0;
        break;
      case MediaLanguageCode:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.MediaLangCode"] = buf.read_uint16(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.MediaLangCode"] = buf.read_uint16(0, bigEndian);
        break;

      default:
        break;
    }
  }
  io_->readOrThrow(buf.data(), size % 4);
}  // QuickTimeVideo::mediaHeaderDecoder

void QuickTimeVideo::trackHeaderDecoder(size_t size) {
  DataBuf buf(5);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';
  int64_t temp = 0;

  for (int i = 0; size / 4 != 0; size -= 4, i++) {
    io_->readOrThrow(buf.data(), 4);

    switch (i) {
      case TrackHeaderVersion:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackHeaderVersion"] = static_cast<int>(buf.read_uint8(0));
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.TrackHeaderVersion"] = static_cast<int>(buf.read_uint8(0));
        break;
      case TrackCreateDate:
        // A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the movie atom was created.
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackCreateDate"] = buf.read_uint32(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.TrackCreateDate"] = buf.read_uint32(0, bigEndian);
        break;
      case TrackModifyDate:
        // A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the movie atom was created.
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackModifyDate"] = buf.read_uint32(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.TrackModifyDate"] = buf.read_uint32(0, bigEndian);
        break;
      case TrackID:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackID"] = buf.read_uint32(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.TrackID"] = buf.read_uint32(0, bigEndian);
        break;
      case TrackDuration:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackDuration"] = timeScale_ ? buf.read_uint32(0, bigEndian) / timeScale_ : 0;
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.TrackDuration"] = timeScale_ ? buf.read_uint32(0, bigEndian) / timeScale_ : 0;
        break;
      case TrackLayer:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackLayer"] = buf.read_uint16(0, bigEndian);
        else if (currentStream_ == Audio)
          xmpData_["Xmp.audio.TrackLayer"] = buf.read_uint16(0, bigEndian);
        break;
      case TrackVolume:
        if (currentStream_ == Video)
          xmpData_["Xmp.video.TrackVolume"] = (static_cast<int>(buf.read_uint8(0)) + (buf.data()[2] * 0.1)) * 100;
        else if (currentStream_ == Audio)
          xmpData_["Xmp.video.TrackVolume"] = (static_cast<int>(buf.read_uint8(0)) + (buf.data()[2] * 0.1)) * 100;
        break;
      case ImageWidth:
        if (currentStream_ == Video) {
          temp = buf.read_uint16(0, bigEndian) + static_cast<int64_t>((buf.data()[2] * 256 + buf.data()[3]) * 0.01);
          xmpData_["Xmp.video.Width"] = temp;
          width_ = temp;
        }
        break;
      case ImageHeight:
        if (currentStream_ == Video) {
          temp = buf.read_uint16(0, bigEndian) + static_cast<int64_t>((buf.data()[2] * 256 + buf.data()[3]) * 0.01);
          xmpData_["Xmp.video.Height"] = temp;
          height_ = temp;
        }
        break;
      default:
        break;
    }
  }
  io_->readOrThrow(buf.data(), size % 4);
}  // QuickTimeVideo::trackHeaderDecoder

void QuickTimeVideo::movieHeaderDecoder(size_t size) {
  DataBuf buf(5);
  std::memset(buf.data(), 0x0, buf.size());
  buf.data()[4] = '\0';

  for (int i = 0; size / 4 != 0; size -= 4, i++) {
    io_->readOrThrow(buf.data(), 4);

    switch (i) {
      case MovieHeaderVersion:
        xmpData_["Xmp.video.MovieHeaderVersion"] = static_cast<int>(buf.read_uint8(0));
        break;
      case CreateDate:
        // A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the movie atom was created.
        xmpData_["Xmp.video.DateUTC"] = buf.read_uint32(0, bigEndian);
        break;
      case ModifyDate:
        // A 32-bit integer that specifies (in seconds since midnight, January 1, 1904) when the movie atom was created.
        xmpData_["Xmp.video.ModificationDate"] = buf.read_uint32(0, bigEndian);
        break;
      case TimeScale:
        xmpData_["Xmp.video.TimeScale"] = buf.read_uint32(0, bigEndian);
        timeScale_ = buf.read_uint32(0, bigEndian);
        if (timeScale_ <= 0)
          timeScale_ = 1;
        break;
      case Duration:
        if (timeScale_ != 0) {  // To prevent division by zero
          xmpData_["Xmp.video.Duration"] = buf.read_uint32(0, bigEndian) * 1000 / timeScale_;
        }
        break;
      case PreferredRate:
        xmpData_["Xmp.video.PreferredRate"] =
            buf.read_uint16(0, bigEndian) + ((buf.data()[2] * 256 + buf.data()[3]) * 0.01);
        break;
      case PreferredVolume:
        xmpData_["Xmp.video.PreferredVolume"] = (static_cast<int>(buf.read_uint8(0)) + (buf.data()[2] * 0.1)) * 100;
        break;
      case PreviewTime:
        xmpData_["Xmp.video.PreviewTime"] = buf.read_uint32(0, bigEndian);
        break;
      case PreviewDuration:
        xmpData_["Xmp.video.PreviewDuration"] = buf.read_uint32(0, bigEndian);
        break;
      case PosterTime:
        xmpData_["Xmp.video.PosterTime"] = buf.read_uint32(0, bigEndian);
        break;
      case SelectionTime:
        xmpData_["Xmp.video.SelectionTime"] = buf.read_uint32(0, bigEndian);
        break;
      case SelectionDuration:
        xmpData_["Xmp.video.SelectionDuration"] = buf.read_uint32(0, bigEndian);
        break;
      case CurrentTime:
        xmpData_["Xmp.video.CurrentTime"] = buf.read_uint32(0, bigEndian);
        break;
      case NextTrackID:
        xmpData_["Xmp.video.NextTrackID"] = buf.read_uint32(0, bigEndian);
        break;
      default:
        break;
    }
  }
  io_->readOrThrow(buf.data(), size % 4);
}  // QuickTimeVideo::movieHeaderDecoder

Image::UniquePtr newQTimeInstance(BasicIo::UniquePtr io, bool /*create*/) {
  auto image = std::make_unique<QuickTimeVideo>(std::move(io));
  if (!image->good()) {
    return nullptr;
  }
  return image;
}

bool isQTimeType(BasicIo& iIo, bool advance) {
  auto buf = DataBuf(12);
  iIo.read(buf.data(), 12);

  if (iIo.error() || iIo.eof()) {
    return false;
  }
  auto qTimeTags = std::array{"PICT", "free", "ftyp", "junk", "mdat", "moov", "pict", "pnot", "skip", "uuid", "wide"};

  bool matched = false;

  for (auto const& tag : qTimeTags) {
    auto tmp = buf.cmpBytes(4, tag, 4);
    if (tmp == 0) {
      // we only match if we actually know the video type. This is done
      // to avoid matching just on ftyp because bmffimage also has that
      // header.
      if (Exiv2::find(qTimeFileType, std::string{buf.c_str(8), 4})) {
        matched = true;
      }
      break;
    }
  }

  if (!advance || !matched) {
    iIo.seek(static_cast<long>(0), BasicIo::beg);
  }

  return matched;
}

}  // namespace Exiv2
