"""
EXIF metadata parser; also parses TIFF file headers.

Author: Victor Stinner, Robert Xiao

References:
- Exif 2.2 Specification (JEITA CP-3451)
    http://www.exif.org/Exif2-2.PDF
- TIFF 6.0 Specification
    http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
"""

from hachoir.field import (FieldSet, SeekableFieldSet, ParserError,
                               UInt8, UInt16, UInt32,
                               Int8, Int16, Int32,
                               Float32, Float64,
                               Enum, String, Bytes, SubFile,
                               NullBits, NullBytes)
from hachoir.core.endian import LITTLE_ENDIAN, BIG_ENDIAN
from hachoir.core.tools import createDict


def rationalFactory(class_name, size, field_class):
    class Rational(FieldSet):
        static_size = size

        def createFields(self):
            yield field_class(self, "numerator")
            yield field_class(self, "denominator")

        def createValue(self):
            return float(self["numerator"].value) / self["denominator"].value
    cls = Rational
    cls.__name__ = class_name
    return cls


RationalInt32 = rationalFactory("RationalInt32", 64, Int32)
RationalUInt32 = rationalFactory("RationalUInt32", 64, UInt32)


class ASCIIString(String):

    def __init__(self, parent, name, nbytes, description=None, strip=' \0', charset='ISO-8859-1', *args, **kwargs):
        String.__init__(self, parent, name, nbytes, description,
                        strip, charset, *args, **kwargs)


class IFDTag(UInt16):

    def getTag(self):
        return self.parent.TAG_INFO.get(self.value, (hex(self.value), hex(self.value)))

    def createDisplay(self):
        return self.getTag()[0]


class ValueArray(FieldSet):

    def __init__(self, parent, name, type, count, description=None):
        FieldSet.__init__(self, parent, name, size=type.static_size * count, description=description)
        self.array_type = type
        self.array_count = count

    def createFields(self):
        for i in range(self.array_count):
            yield self.array_type(self, "data[]")

    def createValue(self):
        return [f.value for f in self]

    def createDisplay(self):
        if self.array_count > 16:
            return '<Array of %d %s>' % (self.array_count, self.array_type.__name__)
        else:
            return '[' + ', '.join(f.display for f in self) + ']'


class BasicIFDEntry(FieldSet):
    TYPE_BYTE = 0
    TYPE_UNDEFINED = 7
    TYPE_RATIONAL = 5
    TYPE_SIGNED_RATIONAL = 10
    TYPE_INFO = {
        1: (UInt8, "BYTE (8 bits)"),
        2: (ASCIIString, "ASCII (8 bits)"),
        3: (UInt16, "SHORT (16 bits)"),
        4: (UInt32, "LONG (32 bits)"),
        5: (RationalUInt32, "RATIONAL (2x LONG, 64 bits)"),
        6: (Int8, "SBYTE (8 bits)"),
        7: (Bytes, "UNDEFINED (8 bits)"),
        8: (Int16, "SSHORT (16 bits)"),
        9: (Int32, "SLONG (32 bits)"),
        10: (RationalInt32, "SRATIONAL (2x SLONG, 64 bits)"),
        11: (Float32, "FLOAT (32 bits)"),
        12: (Float64, "DOUBLE (64 bits)"),
    }
    ENTRY_FORMAT = createDict(TYPE_INFO, 0)
    TYPE_NAME = createDict(TYPE_INFO, 1)
    TAG_INFO = {}

    def createFields(self):
        yield IFDTag(self, "tag", "Tag")
        yield Enum(UInt16(self, "type", "Type"), self.TYPE_NAME)
        self.value_cls = self.ENTRY_FORMAT.get(self['type'].value, Bytes)
        if issubclass(self.value_cls, Bytes):
            self.value_size = 8
        else:
            self.value_size = self.value_cls.static_size
        yield UInt32(self, "count", "Count")

        count = self['count'].value
        totalsize = self.value_size * count
        if count == 0:
            yield NullBytes(self, "padding", 4)
        elif totalsize <= 32:
            name = "value"
            if issubclass(self.value_cls, Bytes):
                yield self.value_cls(self, name, count)
            elif count == 1:
                yield self.value_cls(self, name)
            else:
                yield ValueArray(self, name, self.value_cls, count)
            if totalsize < 32:
                yield NullBits(self, "padding", 32 - totalsize)
        else:
            yield UInt32(self, "offset", "Value offset")

    def createValue(self):
        if "value" in self:
            return self['value'].value
        return None

    def createDescription(self):
        return "Entry: " + self["tag"].getTag()[1]


class IFDEntry(BasicIFDEntry):
    EXIF_IFD_POINTER = 0x8769
    GPS_IFD_POINTER = 0x8825
    INTEROP_IFD_POINTER = 0xA005
    SUBIFD_POINTERS = 0x014A

    TAG_INFO = {
        # image data structure
        0x0100: ("ImageWidth", "Image width"),
        0x0101: ("ImageLength", "Image height"),
        0x0102: ("BitsPerSample", "Number of bits per component"),
        0x0103: ("Compression", "Compression scheme"),
        0x0106: ("PhotometricInterpretation", "Pixel composition"),
        0x0112: ("Orientation", "Orientation of image"),
        0x0115: ("SamplesPerPixel", "Number of components"),
        0x011C: ("PlanarConfiguration", "Image data arrangement"),
        0x0212: ("YCbCrSubSampling", "Subsampling ratio of Y to C"),
        0x0213: ("YCbCrPositioning", "Y and C positioning"),
        0x011A: ("XResolution", "Image resolution in width direction"),
        0x011B: ("YResolution", "Image resolution in height direction"),
        0x0128: ("ResolutionUnit", "Unit of X and Y resolution"),
        # recording offset
        0x0111: ("StripOffsets", "Image data location"),
        0x0116: ("RowsPerStrip", "Number of rows per strip"),
        0x0117: ("StripByteCounts", "Bytes per compressed strip"),
        0x0201: ("JPEGInterchangeFormat", "Offset to JPEG SOI"),
        0x0202: ("JPEGInterchangeFormatLength", "Bytes of JPEG data"),
        # image data characteristics
        0x012D: ("TransferFunction", "Transfer function"),
        0x013E: ("WhitePoint", "White point chromaticity"),
        0x013F: ("PrimaryChromaticities", "Chromaticities of primaries"),
        0x0211: ("YCbCrCoefficients", "Color space transformation matrix coefficients"),
        0x0214: ("ReferenceBlackWhite", "Pair of black and white reference values"),
        # other tags
        0x0132: ("DateTime", "File change date and time"),
        0x010E: ("ImageDescription", "Image title"),
        0x010F: ("Make", "Image input equipment manufacturer"),
        0x0110: ("Model", "Image input equipment model"),
        0x0131: ("Software", "Software used"),
        0x013B: ("Artist", "Person who created the image"),
        0x8298: ("Copyright", "Copyright holder"),
        0x02bc: ("XMPPacket", "XMP Packet"),
        # TIFF-specific tags
        0x00FE: ("NewSubfileType", "NewSubfileType"),
        0x00FF: ("SubfileType", "SubfileType"),
        0x0107: ("Threshholding", "Threshholding"),
        0x0108: ("CellWidth", "CellWidth"),
        0x0109: ("CellLength", "CellLength"),
        0x010A: ("FillOrder", "FillOrder"),
        0x010D: ("DocumentName", "DocumentName"),
        0x0118: ("MinSampleValue", "MinSampleValue"),
        0x0119: ("MaxSampleValue", "MaxSampleValue"),
        0x011D: ("PageName", "PageName"),
        0x011E: ("XPosition", "XPosition"),
        0x011F: ("YPosition", "YPosition"),
        0x0120: ("FreeOffsets", "FreeOffsets"),
        0x0121: ("FreeByteCounts", "FreeByteCounts"),
        0x0122: ("GrayResponseUnit", "GrayResponseUnit"),
        0x0123: ("GrayResponseCurve", "GrayResponseCurve"),
        0x0124: ("T4Options", "T4Options"),
        0x0125: ("T6Options", "T6Options"),
        0x0129: ("PageNumber", "PageNumber"),
        0x013C: ("HostComputer", "HostComputer"),
        0x013D: ("Predictor", "Predictor"),
        0x0140: ("ColorMap", "ColorMap"),
        0x0141: ("HalftoneHints", "HalftoneHints"),
        0x0142: ("TileWidth", "TileWidth"),
        0x0143: ("TileLength", "TileLength"),
        0x0144: ("TileOffsets", "TileOffsets"),
        0x0145: ("TileByteCounts", "TileByteCounts"),
        SUBIFD_POINTERS: ("SubIFDs", "SubIFDs"),
        0x014C: ("InkSet", "InkSet"),
        0x014D: ("InkNames", "InkNames"),
        0x014E: ("NumberOfInks", "NumberOfInks"),
        0x0150: ("DotRange", "DotRange"),
        0x0151: ("TargetPrinter", "TargetPrinter"),
        0x0152: ("ExtraSamples", "ExtraSamples"),
        0x0153: ("SampleFormat", "SampleFormat"),
        0x0154: ("SMinSampleValue", "SMinSampleValue"),
        0x0155: ("SMaxSampleValue", "SMaxSampleValue"),
        0x0156: ("TransferRange", "TransferRange"),
        0x0200: ("JPEGProc", "JPEGProc"),
        0x0203: ("JPEGRestartInterval", "JPEGRestartInterval"),
        0x0205: ("JPEGLosslessPredictors", "JPEGLosslessPredictors"),
        0x0206: ("JPEGPointTransforms", "JPEGPointTransforms"),
        0x0207: ("JPEGQTables", "JPEGQTables"),
        0x0208: ("JPEGDCTables", "JPEGDCTables"),
        0x0209: ("JPEGACTables", "JPEGACTables"),
        # IFD pointers
        EXIF_IFD_POINTER: ("IFDExif", "Exif IFD Pointer"),
        GPS_IFD_POINTER: ("IFDGPS", "GPS IFD Pointer"),
        INTEROP_IFD_POINTER: ("IFDInterop", "Interoperability IFD Pointer"),
    }


class ExifIFDEntry(BasicIFDEntry):
    TAG_INFO = {
        # version
        0x9000: ("ExifVersion", "Exif version"),
        0xA000: ("FlashpixVersion", "Supported Flashpix version"),
        # image data characteristics
        0xA001: ("ColorSpace", "Color space information"),
        # image configuration
        0x9101: ("ComponentsConfiguration", "Meaning of each component"),
        0x9102: ("CompressedBitsPerPixel", "Image compression mode"),
        0xA002: ("PixelXDimension", "Valid image width"),
        0xA003: ("PixelYDimension", "Valid image height"),
        # user information
        0x927C: ("MakerNote", "Manufacturer notes"),
        0x9286: ("UserComment", "User comments"),
        # related file information
        0xA004: ("RelatedSoundFile", "Related audio file"),
        # date and time
        0x9003: ("DateTimeOriginal", "Date and time of original data generation"),
        0x9004: ("DateTimeDigitized", "Date and time of digital data generation"),
        0x9290: ("SubSecTime", "DateTime subseconds"),
        0x9291: ("SubSecTimeOriginal", "DateTimeOriginal subseconds"),
        0x9292: ("SubSecTimeDigitized", "DateTimeDigitized subseconds"),
        # picture-taking conditions
        0x829A: ("ExposureTime", "Exposure time"),
        0x829D: ("FNumber", "F number"),
        0x8822: ("ExposureProgram", "Exposure program"),
        0x8824: ("SpectralSensitivity", "Spectral sensitivity"),
        0x8827: ("ISOSpeedRatings", "ISO speed rating"),
        0x8828: ("OECF", "Optoelectric conversion factor"),
        0x9201: ("ShutterSpeedValue", "Shutter speed"),
        0x9202: ("ApertureValue", "Aperture"),
        0x9203: ("BrightnessValue", "Brightness"),
        0x9204: ("ExposureBiasValue", "Exposure bias"),
        0x9205: ("MaxApertureValue", "Maximum lens aperture"),
        0x9206: ("SubjectDistance", "Subject distance"),
        0x9207: ("MeteringMode", "Metering mode"),
        0x9208: ("LightSource", "Light source"),
        0x9209: ("Flash", "Flash"),
        0x920A: ("FocalLength", "Lens focal length"),
        0x9214: ("SubjectArea", "Subject area"),
        0xA20B: ("FlashEnergy", "Flash energy"),
        0xA20C: ("SpatialFrequencyResponse", "Spatial frequency response"),
        0xA20E: ("FocalPlaneXResolution", "Focal plane X resolution"),
        0xA20F: ("FocalPlaneYResolution", "Focal plane Y resolution"),
        0xA210: ("FocalPlaneResolutionUnit", "Focal plane resolution unit"),
        0xA214: ("SubjectLocation", "Subject location"),
        0xA215: ("ExposureIndex", "Exposure index"),
        0xA217: ("SensingMethod", "Sensing method"),
        0xA300: ("FileSource", "File source"),
        0xA301: ("SceneType", "Scene type"),
        0xA302: ("CFAPattern", "CFA pattern"),
        0xA401: ("CustomRendered", "Custom image processing"),
        0xA402: ("ExposureMode", "Exposure mode"),
        0xA403: ("WhiteBalance", "White balance"),
        0xA404: ("DigitalZoomRatio", "Digital zoom ratio"),
        0xA405: ("FocalLengthIn35mmFilm", "Focal length in 35 mm film"),
        0xA406: ("SceneCaptureType", "Scene capture type"),
        0xA407: ("GainControl", "Gain control"),
        0xA408: ("Contrast", "Contrast"),
        0xA409: ("Saturation", "Saturation"),
        0xA40A: ("Sharpness", "Sharpness"),
        0xA40B: ("DeviceSettingDescription", "Device settings description"),
        0xA40C: ("SubjectDistanceRange", "Subject distance range"),
        # other tags
        0xA420: ("ImageUniqueID", "Unique image ID"),
    }


class GPSIFDEntry(BasicIFDEntry):
    TAG_INFO = {
        0x0000: ("GPSVersionID", "GPS tag version"),
        0x0001: ("GPSLatitudeRef", "North or South Latitude"),
        0x0002: ("GPSLatitude", "Latitude"),
        0x0003: ("GPSLongitudeRef", "East or West Longitude"),
        0x0004: ("GPSLongitude", "Longitude"),
        0x0005: ("GPSAltitudeRef", "Altitude reference"),
        0x0006: ("GPSAltitude", "Altitude"),
        0x0007: ("GPSTimeStamp", "GPS time (atomic clock)"),
        0x0008: ("GPSSatellites", "GPS satellites used for measurement"),
        0x0009: ("GPSStatus", "GPS receiver status"),
        0x000A: ("GPSMeasureMode", "GPS measurement mode"),
        0x000B: ("GPSDOP", "Measurement precision"),
        0x000C: ("GPSSpeedRef", "Speed unit"),
        0x000D: ("GPSSpeed", "Speed of GPS receiver"),
        0x000E: ("GPSTrackRef", "Reference for direction of movement"),
        0x000F: ("GPSTrack", "Direction of movement"),
        0x0010: ("GPSImgDirectionRef", "Reference for direction of image"),
        0x0011: ("GPSImgDirection", "Direction of image"),
        0x0012: ("GPSMapDatum", "Geodetic survey data used"),
        0x0013: ("GPSDestLatitudeRef", "Reference for latitude of destination"),
        0x0014: ("GPSDestLatitude", "Latitude of destination"),
        0x0015: ("GPSDestLongitudeRef", "Reference for longitude of destination"),
        0x0016: ("GPSDestLongitude", "Longitude of destination"),
        0x0017: ("GPSDestBearingRef", "Reference for bearing of destination"),
        0x0018: ("GPSDestBearing", "Bearing of destination"),
        0x0019: ("GPSDestDistanceRef", "Reference for distance to destination"),
        0x001A: ("GPSDestDistance", "Distance to destination"),
        0x001B: ("GPSProcessingMethod", "Name of GPS processing method"),
        0x001C: ("GPSAreaInformation", "Name of GPS area"),
        0x001D: ("GPSDateStamp", "GPS date"),
        0x001E: ("GPSDifferential", "GPS differential correction"),
    }


class InteropIFDEntry(BasicIFDEntry):
    TAG_INFO = {
        0x0001: ("InteroperabilityIndex", "Interoperability Identification"),
    }


class IFD(SeekableFieldSet):
    EntryClass = IFDEntry

    def __init__(self, parent, name, base_addr):
        self.base_addr = base_addr
        SeekableFieldSet.__init__(self, parent, name)

    def createFields(self):
        yield UInt16(self, "count", "Number of entries")
        count = self["count"].value
        if count == 0:
            raise ParserError("IFDs cannot be empty.")
        for i in range(count):
            yield self.EntryClass(self, "entry[]")
        yield UInt32(self, "next", "Offset to next IFD")
        for i in range(count):
            entry = self['entry[%d]' % i]
            if 'offset' not in entry:
                continue
            self.seekByte(entry['offset'].value +
                          self.base_addr // 8, relative=False)
            count = entry['count'].value
            name = "value[%s]" % i
            if issubclass(entry.value_cls, Bytes):
                yield entry.value_cls(self, name, count)
            elif entry['tag'].display == "XMPPacket":
                yield String(self, name, count)
            elif count == 1:
                yield entry.value_cls(self, name)
            else:
                yield ValueArray(self, name, entry.value_cls, count)

    def getEntryValues(self, entry):
        n = int(entry.name.rsplit('[', 1)[1].strip(']'))
        if 'offset' in entry:
            field = 'value[%d]' % n
            base = self
        else:
            field = 'value'
            base = entry
        if field not in base:
            return None
        elif isinstance(base[field], ValueArray):
            return list(base[field])
        else:
            return [base[field]]


class ExifIFD(IFD):
    EntryClass = ExifIFDEntry


class GPSIFD(IFD):
    EntryClass = GPSIFDEntry


class InteropIFD(IFD):
    EntryClass = InteropIFDEntry


IFD_TAGS = {
    IFDEntry.EXIF_IFD_POINTER: ('exif', ExifIFD),
    IFDEntry.GPS_IFD_POINTER: ('exif_gps', GPSIFD),
    IFDEntry.INTEROP_IFD_POINTER: ('exif_interop', InteropIFD),
}


def TIFF(self):
    iff_start = self.absolute_address + self.current_size
    yield String(self, "endian", 2, "Endian ('II' or 'MM')", charset="ASCII")
    if self["endian"].value not in ("II", "MM"):
        raise ParserError("Invalid endian!")
    if self["endian"].value == "II":
        self.endian = LITTLE_ENDIAN
    else:
        self.endian = BIG_ENDIAN

    yield UInt16(self, "version", "TIFF version number")
    yield UInt32(self, "img_dir_ofs", "Next image directory offset")
    offsets = [(self['img_dir_ofs'].value, 'ifd[]', IFD)]
    while offsets:
        offset, name, klass = offsets.pop(0)
        self.seekByte(offset + iff_start // 8, relative=False)
        ifd = klass(self, name, iff_start)
        yield ifd
        for entry in ifd.array('entry'):
            tag = entry['tag'].value
            if tag in IFD_TAGS:
                name, klass = IFD_TAGS[tag]
                offsets.append((ifd.getEntryValues(entry)[
                               0].value, name + '[]', klass))
            elif tag == IFDEntry.SUBIFD_POINTERS:
                for val in ifd.getEntryValues(entry):
                    offsets.append((val.value, ifd.name + '[]', IFD))

        if ifd['next'].value != 0:
            offsets.append((ifd['next'].value, 'ifd[]', IFD))


class Exif(SeekableFieldSet):

    def createFields(self):
        # Headers
        yield String(self, "header", 6, "Header (Exif\\0\\0)", charset="ASCII")
        if self["header"].value != "Exif\0\0":
            raise ParserError("Invalid EXIF signature!")
        iff_start = self.absolute_address + self.current_size
        ifds = []
        for field in TIFF(self):
            yield field
            if isinstance(field, IFD):
                ifds.append(field)

        for ifd in ifds:
            data = {}
            for i, entry in enumerate(ifd.array('entry')):
                data[entry['tag'].display] = entry
            if 'JPEGInterchangeFormat' in data and 'JPEGInterchangeFormatLength' in data:
                offs = ifd.getEntryValues(
                    data['JPEGInterchangeFormat'])[0].value
                size = ifd.getEntryValues(
                    data['JPEGInterchangeFormatLength'])[0].value
                if size == 0:
                    continue
                self.seekByte(offs + iff_start // 8, relative=False)
                yield SubFile(self, "thumbnail[]", size, "Thumbnail (JPEG file)", mime_type="image/jpeg")