# Created By: Virgil Dupras
# Created On: 2011-04-20
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html

# Heavily based on http://topo.math.u-psud.fr/~bousch/exifdump.py by Thierry Bousch (Public Domain)

import logging

EXIF_TAGS = {
    0x0100: "ImageWidth",
    0x0101: "ImageLength",
    0x0102: "BitsPerSample",
    0x0103: "Compression",
    0x0106: "PhotometricInterpretation",
    0x010A: "FillOrder",
    0x010D: "DocumentName",
    0x010E: "ImageDescription",
    0x010F: "Make",
    0x0110: "Model",
    0x0111: "StripOffsets",
    0x0112: "Orientation",
    0x0115: "SamplesPerPixel",
    0x0116: "RowsPerStrip",
    0x0117: "StripByteCounts",
    0x011A: "XResolution",
    0x011B: "YResolution",
    0x011C: "PlanarConfiguration",
    0x0128: "ResolutionUnit",
    0x012D: "TransferFunction",
    0x0131: "Software",
    0x0132: "DateTime",
    0x013B: "Artist",
    0x013E: "WhitePoint",
    0x013F: "PrimaryChromaticities",
    0x0156: "TransferRange",
    0x0200: "JPEGProc",
    0x0201: "JPEGInterchangeFormat",
    0x0202: "JPEGInterchangeFormatLength",
    0x0211: "YCbCrCoefficients",
    0x0212: "YCbCrSubSampling",
    0x0213: "YCbCrPositioning",
    0x0214: "ReferenceBlackWhite",
    0x828F: "BatteryLevel",
    0x8298: "Copyright",
    0x829A: "ExposureTime",
    0x829D: "FNumber",
    0x83BB: "IPTC/NAA",
    0x8769: "ExifIFDPointer",
    0x8773: "InterColorProfile",
    0x8822: "ExposureProgram",
    0x8824: "SpectralSensitivity",
    0x8825: "GPSInfoIFDPointer",
    0x8827: "ISOSpeedRatings",
    0x8828: "OECF",
    0x9000: "ExifVersion",
    0x9003: "DateTimeOriginal",
    0x9004: "DateTimeDigitized",
    0x9101: "ComponentsConfiguration",
    0x9102: "CompressedBitsPerPixel",
    0x9201: "ShutterSpeedValue",
    0x9202: "ApertureValue",
    0x9203: "BrightnessValue",
    0x9204: "ExposureBiasValue",
    0x9205: "MaxApertureValue",
    0x9206: "SubjectDistance",
    0x9207: "MeteringMode",
    0x9208: "LightSource",
    0x9209: "Flash",
    0x920A: "FocalLength",
    0x9214: "SubjectArea",
    0x927C: "MakerNote",
    0x9286: "UserComment",
    0x9290: "SubSecTime",
    0x9291: "SubSecTimeOriginal",
    0x9292: "SubSecTimeDigitized",
    0xA000: "FlashPixVersion",
    0xA001: "ColorSpace",
    0xA002: "PixelXDimension",
    0xA003: "PixelYDimension",
    0xA004: "RelatedSoundFile",
    0xA005: "InteroperabilityIFDPointer",
    0xA20B: "FlashEnergy",  # 0x920B in TIFF/EP
    0xA20C: "SpatialFrequencyResponse",  # 0x920C    -  -
    0xA20E: "FocalPlaneXResolution",  # 0x920E    -  -
    0xA20F: "FocalPlaneYResolution",  # 0x920F    -  -
    0xA210: "FocalPlaneResolutionUnit",  # 0x9210    -  -
    0xA214: "SubjectLocation",  # 0x9214    -  -
    0xA215: "ExposureIndex",  # 0x9215    -  -
    0xA217: "SensingMethod",  # 0x9217    -  -
    0xA300: "FileSource",
    0xA301: "SceneType",
    0xA302: "CFAPattern",  # 0x828E in TIFF/EP
    0xA401: "CustomRendered",
    0xA402: "ExposureMode",
    0xA403: "WhiteBalance",
    0xA404: "DigitalZoomRatio",
    0xA405: "FocalLengthIn35mmFilm",
    0xA406: "SceneCaptureType",
    0xA407: "GainControl",
    0xA408: "Contrast",
    0xA409: "Saturation",
    0xA40A: "Sharpness",
    0xA40B: "DeviceSettingDescription",
    0xA40C: "SubjectDistanceRange",
    0xA420: "ImageUniqueID",
}

INTR_TAGS = {
    0x0001: "InteroperabilityIndex",
    0x0002: "InteroperabilityVersion",
    0x1000: "RelatedImageFileFormat",
    0x1001: "RelatedImageWidth",
    0x1002: "RelatedImageLength",
}

GPS_TA0GS = {
    0x00: "GPSVersionID",
    0x01: "GPSLatitudeRef",
    0x02: "GPSLatitude",
    0x03: "GPSLongitudeRef",
    0x04: "GPSLongitude",
    0x05: "GPSAltitudeRef",
    0x06: "GPSAltitude",
    0x07: "GPSTimeStamp",
    0x08: "GPSSatellites",
    0x09: "GPSStatus",
    0x0A: "GPSMeasureMode",
    0x0B: "GPSDOP",
    0x0C: "GPSSpeedRef",
    0x0D: "GPSSpeed",
    0x0E: "GPSTrackRef",
    0x0F: "GPSTrack",
    0x10: "GPSImgDirectionRef",
    0x11: "GPSImgDirection",
    0x12: "GPSMapDatum",
    0x13: "GPSDestLatitudeRef",
    0x14: "GPSDestLatitude",
    0x15: "GPSDestLongitudeRef",
    0x16: "GPSDestLongitude",
    0x17: "GPSDestBearingRef",
    0x18: "GPSDestBearing",
    0x19: "GPSDestDistanceRef",
    0x1A: "GPSDestDistance",
    0x1B: "GPSProcessingMethod",
    0x1C: "GPSAreaInformation",
    0x1D: "GPSDateStamp",
    0x1E: "GPSDifferential",
}

INTEL_ENDIAN = ord("I")
MOTOROLA_ENDIAN = ord("M")

# About MAX_COUNT: It's possible to have corrupted exif tags where the entry count is way too high
# and thus makes us loop, not endlessly, but for heck of a long time for nothing. Therefore, we put
# an arbitrary limit on the entry count we'll allow ourselves to read and any IFD reporting more
# entries than that will be considered corrupt.
MAX_COUNT = 0xFFFF


def s2n_motorola(bytes):
    x = 0
    for c in bytes:
        x = (x << 8) | c
    return x


def s2n_intel(bytes):
    x = 0
    y = 0
    for c in bytes:
        x = x | (c << y)
        y = y + 8
    return x


class Fraction:
    def __init__(self, num, den):
        self.num = num
        self.den = den

    def __repr__(self):
        return "%d/%d" % (self.num, self.den)


class TIFF_file:
    def __init__(self, data):
        self.data = data
        self.endian = data[0]
        self.s2nfunc = s2n_intel if self.endian == INTEL_ENDIAN else s2n_motorola

    def s2n(self, offset, length, signed=0, debug=False):
        slice = self.data[offset : offset + length]
        val = self.s2nfunc(slice)
        # Sign extension ?
        if signed:
            msb = 1 << (8 * length - 1)
            if val & msb:
                val = val - (msb << 1)
        if debug:
            logging.debug(self.endian)
            logging.debug(
                "Slice for offset %d length %d: %r and value: %d",
                offset,
                length,
                slice,
                val,
            )
        return val

    def first_IFD(self):
        return self.s2n(4, 4)

    def next_IFD(self, ifd):
        entries = self.s2n(ifd, 2)
        return self.s2n(ifd + 2 + 12 * entries, 4)

    def list_IFDs(self):
        i = self.first_IFD()
        a = []
        while i:
            a.append(i)
            i = self.next_IFD(i)
        return a

    def dump_IFD(self, ifd):
        entries = self.s2n(ifd, 2)
        logging.debug("Entries for IFD %d: %d", ifd, entries)
        if entries > MAX_COUNT:
            logging.debug("Probably corrupt. Aborting.")
            return []
        a = []
        for i in range(entries):
            entry = ifd + 2 + 12 * i
            tag = self.s2n(entry, 2)
            type = self.s2n(entry + 2, 2)
            if not 1 <= type <= 10:
                continue  # not handled
            typelen = [1, 1, 2, 4, 8, 1, 1, 2, 4, 8][type - 1]
            count = self.s2n(entry + 4, 4)
            if count > MAX_COUNT:
                logging.debug("Probably corrupt. Aborting.")
                return []
            offset = entry + 8
            if count * typelen > 4:
                offset = self.s2n(offset, 4)
            if type == 2:
                # Special case: nul-terminated ASCII string
                values = str(self.data[offset : offset + count - 1], encoding="latin-1")
            else:
                values = []
                signed = type == 6 or type >= 8
                for j in range(count):
                    if type in {5, 10}:
                        # The type is either 5 or 10
                        value_j = Fraction(
                            self.s2n(offset, 4, signed), self.s2n(offset + 4, 4, signed)
                        )
                    else:
                        # Not a fraction
                        value_j = self.s2n(offset, typelen, signed)
                    values.append(value_j)
                    offset = offset + typelen
            # Now "values" is either a string or an array
            a.append((tag, type, values))
        return a


def read_exif_header(fp):
    # If `fp`'s first bytes are not exif, it tries to find it in the next 4kb
    def isexif(data):
        return data[0:4] == b"\377\330\377\341" and data[6:10] == b"Exif"

    data = fp.read(12)
    if isexif(data):
        return data
    # ok, not exif, try to find it
    large_data = fp.read(4096)
    try:
        index = large_data.index(b"Exif")
        data = large_data[index - 6 : index + 6]
        # large_data omits the first 12 bytes, and the index is at the middle of the header, so we
        # must seek index + 18
        fp.seek(index + 18)
        return data
    except ValueError:
        raise ValueError("Not an Exif file")


def get_fields(fp):
    data = read_exif_header(fp)
    length = data[4] * 256 + data[5]
    logging.debug("Exif header length: %d bytes", length)
    data = fp.read(length - 8)
    data_format = data[0]
    logging.debug(
        "%s format", {INTEL_ENDIAN: "Intel", MOTOROLA_ENDIAN: "Motorola"}[data_format]
    )
    T = TIFF_file(data)
    # There may be more than one IFD per file, but we only read the first one because others are
    # most likely thumbnails.
    main_IFD_offset = T.first_IFD()
    result = {}

    def add_tag_to_result(tag, values):
        try:
            stag = EXIF_TAGS[tag]
        except KeyError:
            stag = "0x%04X" % tag
        if stag in result:
            return  # don't overwrite data
        result[stag] = values

    logging.debug("IFD at offset %d", main_IFD_offset)
    IFD = T.dump_IFD(main_IFD_offset)
    exif_off = gps_off = 0
    for tag, type, values in IFD:
        if tag == 0x8769:
            exif_off = values[0]
            continue
        if tag == 0x8825:
            gps_off = values[0]
            continue
        add_tag_to_result(tag, values)
    if exif_off:
        logging.debug("Exif SubIFD at offset %d:", exif_off)
        IFD = T.dump_IFD(exif_off)
        # Recent digital cameras have a little subdirectory
        # here, pointed to by tag 0xA005. Apparently, it's the
        # "Interoperability IFD", defined in Exif 2.1 and DCF.
        intr_off = 0
        for tag, type, values in IFD:
            if tag == 0xA005:
                intr_off = values[0]
                continue
            add_tag_to_result(tag, values)
        if intr_off:
            logging.debug("Exif Interoperability SubSubIFD at offset %d:", intr_off)
            IFD = T.dump_IFD(intr_off)
            for tag, type, values in IFD:
                add_tag_to_result(tag, values)
    if gps_off:
        logging.debug("GPS SubIFD at offset %d:", gps_off)
        IFD = T.dump_IFD(gps_off)
        for tag, type, values in IFD:
            add_tag_to_result(tag, values)
    return result
