/*****************************************************************************/
// Copyright 2006-2019 Adobe Systems Incorporated
// All Rights Reserved.
//
// NOTICE:  Adobe permits you to use, modify, and distribute this file in
// accordance with the terms of the Adobe license agreement accompanying it.
/*****************************************************************************/

#include "dng_iptc.h"

#include "dng_assertions.h"
#include "dng_auto_ptr.h"
#include "dng_memory_stream.h"
#include "dng_stream.h"
#include "dng_utils.h"

/*****************************************************************************/

dng_iptc::dng_iptc ()

    :   fTitle ()

    ,   fUrgency (-1)

    ,   fCategory ()

    ,   fSupplementalCategories ()

    ,   fKeywords ()

    ,   fInstructions ()

    ,   fDateTimeCreated ()

    ,   fDigitalCreationDateTime ()

    ,   fAuthors         ()
    ,   fAuthorsPosition ()

    ,   fCity        ()
    ,   fState       ()
    ,   fCountry     ()
    ,   fCountryCode ()

    ,   fLocation ()

    ,   fTransmissionReference ()

    ,   fHeadline ()

    ,   fCredit ()

    ,   fSource ()

    ,   fCopyrightNotice ()

    ,   fDescription       ()
    ,   fDescriptionWriter ()

    {

    }

/*****************************************************************************/

dng_iptc::~dng_iptc ()
    {

    }

/*****************************************************************************/

bool dng_iptc::IsEmpty () const
    {

    if (fTitle.NotEmpty ())
        {
        return false;
        }

    if (fUrgency >= 0)
        {
        return false;
        }

    if (fCategory.NotEmpty ())
        {
        return false;
        }

    if (fSupplementalCategories.Count () > 0)
        {
        return false;
        }

    if (fKeywords.Count () > 0)
        {
        return false;
        }

    if (fInstructions.NotEmpty ())
        {
        return false;
        }

    if (fDateTimeCreated.IsValid ())
        {
        return false;
        }

    if (fDigitalCreationDateTime.IsValid ())
        {
        return false;
        }

    if (fAuthors.Count () != 0 ||
        fAuthorsPosition.NotEmpty ())
        {
        return false;
        }

    if (fCity   .NotEmpty () ||
        fState  .NotEmpty () ||
        fCountry.NotEmpty ())
        {
        return false;
        }

    if (fCountryCode.NotEmpty ())
        {
        return false;
        }

    if (fLocation.NotEmpty ())
        {
        return false;
        }

    if (fTransmissionReference.NotEmpty ())
        {
        return false;
        }

    if (fHeadline.NotEmpty ())
        {
        return false;
        }

    if (fCredit.NotEmpty ())
        {
        return false;
        }

    if (fSource.NotEmpty ())
        {
        return false;
        }

    if (fCopyrightNotice.NotEmpty ())
        {
        return false;
        }

    if (fDescription      .NotEmpty () ||
        fDescriptionWriter.NotEmpty ())
        {
        return false;
        }

    return true;

    }

/*****************************************************************************/

void dng_iptc::ParseString (dng_stream &stream,
                            dng_string &s,
                            CharSet charSet)
    {

    uint32 length = stream.Get_uint16 ();

    dng_memory_data buffer (length + 1);

    char *c = buffer.Buffer_char ();

    stream.Get (c, length);

    c [length] = 0;

    switch (charSet)
        {

        case kCharSetUTF8:
            {
            s.Set_UTF8 (c);
            break;
            }

        default:
            {
            s.Set_SystemEncoding (c);
            }

        }

    s.SetLineEndingsToNewLines ();

    s.StripLowASCII ();

    s.TrimTrailingBlanks ();

    }

/*****************************************************************************/

void dng_iptc::Parse (const void *blockData,
                      uint32 blockSize,
                      uint64 offsetInOriginalFile)
    {

    dng_stream stream (blockData,
                       blockSize,
                       offsetInOriginalFile);

    stream.SetBigEndian ();

    // Make a first pass though the data, trying to figure out the
    // character set.

    CharSet charSet = kCharSetUnknown;

    bool isValidUTF8 = true;

    bool hasEncodingMarker = false;

    uint64 firstOffset = stream.Position ();

    uint64 nextOffset = firstOffset;

    while (nextOffset + 5 < stream.Length ())
        {

        stream.SetReadPosition (nextOffset);

        uint8 firstByte = stream.Get_uint8 ();

        if (firstByte != 0x1C) break;

        uint8  record   = stream.Get_uint8  ();
        uint8  dataSet  = stream.Get_uint8  ();
        uint32 dataSize = stream.Get_uint16 ();

        nextOffset = stream.Position () + dataSize;

        if (record == 1)
            {

            switch (dataSet)
                {

                case 90:
                    {

                    hasEncodingMarker = true;

                    if (dataSize == 3)
                        {

                        uint32 byte1 = stream.Get_uint8 ();
                        uint32 byte2 = stream.Get_uint8 ();
                        uint32 byte3 = stream.Get_uint8 ();

                        if (byte1 == 27 /* Escape */ &&
                            byte2 == 0x25 &&
                            byte3 == 0x47)
                            {

                            charSet = kCharSetUTF8;

                            }

                        }

                    break;

                    }

                default:
                    break;

                }

            }

        else if (record == 2)
            {

            dng_memory_data buffer (dataSize + 1);

            char *s = buffer.Buffer_char ();

            stream.Get (s, dataSize);

            s [dataSize] = 0;

            isValidUTF8 = isValidUTF8 && dng_string::IsUTF8 (s);

            }

        }

    // If we don't have an encoding marker, and the data is valid
    // UTF-8, then assume that it is UTF-8 (rather than system encoding).

    if (!hasEncodingMarker && isValidUTF8)
        {

        charSet = kCharSetUTF8;

        }

    // Make a second pass though the data, actually reading the data.

    nextOffset = firstOffset;

    while (nextOffset + 5 < stream.Length ())
        {

        stream.SetReadPosition (nextOffset);

        uint8 firstByte = stream.Get_uint8 ();

        if (firstByte != 0x1C) break;

        uint8  record   = stream.Get_uint8  ();
        uint8  dataSet  = stream.Get_uint8  ();
        uint32 dataSize = stream.Get_uint16 ();

        nextOffset = stream.Position () + dataSize;

        if (record == 2)
            {

            stream.SetReadPosition (stream.Position () - 2);

            switch ((DataSet) dataSet)
                {

                case kObjectNameSet:
                    {
                    ParseString (stream, fTitle, charSet);
                    break;
                    }

                case kUrgencySet:
                    {

                    int32 size = stream.Get_uint16 ();

                    if (size == 1)
                        {

                        char c = stream.Get_int8 ();

                        if (c >= '0' && c <= '9')
                            {
                            fUrgency = c - '0';
                            }

                        }

                    break;

                    }

                case kCategorySet:
                    {
                    ParseString (stream, fCategory, charSet);
                    break;
                    }

                case kSupplementalCategoriesSet:
                    {

                    dng_string category;

                    ParseString (stream, category, charSet);

                    if (category.NotEmpty ())
                        {
                        fSupplementalCategories.Append (category);
                        }

                    break;

                    }

                case kKeywordsSet:
                    {

                    dng_string keyword;

                    ParseString (stream, keyword, charSet);

                    if (keyword.NotEmpty ())
                        {
                        fKeywords.Append (keyword);
                        }

                    break;

                    }

                case kSpecialInstructionsSet:
                    {
                    ParseString (stream, fInstructions, charSet);
                    break;
                    }

                case kDateCreatedSet:
                    {

                    uint32 length = stream.Get_uint16 ();

                    if (length == 8)
                        {

                        char date [9];

                        stream.Get (date, 8);

                        date [8] = 0;

                        fDateTimeCreated.Decode_IPTC_Date (date);

                        }

                    break;

                    }

                case kTimeCreatedSet:
                    {

                    uint32 length = stream.Get_uint16 ();

                    if (length >= 4 && length <= 11)
                        {

                        char time [12];

                        stream.Get (time, length);

                        time [length] = 0;

                        fDateTimeCreated.Decode_IPTC_Time (time);

                        }

                    break;

                    }

                case kDigitalCreationDateSet:
                    {

                    uint32 length = stream.Get_uint16 ();

                    if (length == 8)
                        {

                        char date [9];

                        stream.Get (date, 8);

                        date [8] = 0;

                        fDigitalCreationDateTime.Decode_IPTC_Date (date);

                        }

                    break;

                    }

                case kDigitalCreationTimeSet:
                    {

                    uint32 length = stream.Get_uint16 ();

                    if (length >= 4 && length <= 11)
                        {

                        char time [12];

                        stream.Get (time, length);

                        time [length] = 0;

                        fDigitalCreationDateTime.Decode_IPTC_Time (time);

                        }

                    break;

                    }

                case kBylineSet:
                    {

                    dng_string author;

                    ParseString (stream, author, charSet);

                    if (author.NotEmpty ())
                        {
                        fAuthors.Append (author);
                        }

                    break;

                    }

                case kBylineTitleSet:
                    {
                    ParseString (stream, fAuthorsPosition, charSet);
                    break;
                    }

                case kCitySet:
                    {
                    ParseString (stream, fCity, charSet);
                    break;
                    }

                case kProvinceStateSet:
                    {
                    ParseString (stream, fState, charSet);
                    break;
                    }

                case kCountryNameSet:
                    {
                    ParseString (stream, fCountry, charSet);
                    break;
                    }

                case kCountryCodeSet:
                    {
                    ParseString (stream, fCountryCode, charSet);
                    break;
                    }

                case kSublocationSet:
                    {
                    ParseString (stream, fLocation, charSet);
                    break;
                    }

                case kOriginalTransmissionReferenceSet:
                    {
                    ParseString (stream, fTransmissionReference, charSet);
                    break;
                    }

                case kHeadlineSet:
                    {
                    ParseString (stream, fHeadline, charSet);
                    break;
                    }

                case kCreditSet:
                    {
                    ParseString (stream, fCredit, charSet);
                    break;
                    }

                case kSourceSet:
                    {
                    ParseString (stream, fSource, charSet);
                    break;
                    }

                case kCopyrightNoticeSet:
                    {
                    ParseString (stream, fCopyrightNotice, charSet);
                    break;
                    }

                case kCaptionSet:
                    {
                    ParseString (stream, fDescription, charSet);
                    break;
                    }

                case kCaptionWriterSet:
                    {
                    ParseString (stream, fDescriptionWriter, charSet);
                    break;
                    }

                // All other IPTC records are not part of the IPTC core
                // and/or are not kept in sync with XMP tags, so we ignore
                // them.

                default:
                    break;

                }

            }

        }

    }

/*****************************************************************************/

void dng_iptc::SpoolString (dng_stream &stream,
                            const dng_string &s,
                            uint8 dataSet,
                            uint32 maxChars,
                            CharSet charSet)
    {

    if (s.IsEmpty ())
        {
        return;
        }

    stream.Put_uint16 (0x1C02);
    stream.Put_uint8  (dataSet);

    dng_string ss (s);

    ss.SetLineEndingsToReturns ();

    if (charSet == kCharSetUTF8)
        {

        // UTF-8 encoding.

        if (ss.Length () > maxChars)
            {
            ss.Truncate (maxChars);
            }

        uint32 len = ss.Length ();

        stream.Put_uint16 ((uint16) len);

        stream.Put (ss.Get (), len);

        }

    else
        {

        // System character set encoding.

        dng_memory_data buffer;

        uint32 len = ss.Get_SystemEncoding (buffer);

        if (len > maxChars)
            {

            uint32 lower = 0;
            uint32 upper = ss.Length () - 1;

            while (upper > lower)
                {

                uint32 middle = (upper + lower + 1) >> 1;

                dng_string sss (ss);

                sss.Truncate (middle);

                len = sss.Get_SystemEncoding (buffer);

                if (len <= maxChars)
                    {

                    lower = middle;

                    }

                else
                    {

                    upper = middle - 1;

                    }

                }

            ss.Truncate (lower);

            len = ss.Get_SystemEncoding (buffer);

            }

        stream.Put_uint16 ((uint16) len);

        stream.Put (buffer.Buffer_char (), len);

        }

    }
/*****************************************************************************/

dng_memory_block * dng_iptc::Spool (dng_memory_allocator &allocator,
                                    bool padForTIFF)
    {

    uint32 j;

    char s [64];

    dng_memory_stream stream (allocator, NULL, 2048);

    stream.SetBigEndian ();

    // Medata working group - now we just always write UTF-8.

    CharSet charSet = kCharSetUTF8;

    // UTF-8 encoding marker.

    if (charSet == kCharSetUTF8)
        {

        stream.Put_uint16 (0x1C01);
        stream.Put_uint8  (90);
        stream.Put_uint16 (3);
        stream.Put_uint8  (27);
        stream.Put_uint8  (0x25);
        stream.Put_uint8  (0x47);

        }

    stream.Put_uint16 (0x1C02);
    stream.Put_uint8  (kRecordVersionSet);
    stream.Put_uint16 (2);
    stream.Put_uint16 (4);

    SpoolString (stream,
                 fTitle,
                 kObjectNameSet,
                 64,
                 charSet);

    if (fUrgency >= 0)
        {

        snprintf (s, 64, "%1u", (unsigned) fUrgency);

        stream.Put_uint16 (0x1C02);
        stream.Put_uint8  (kUrgencySet);

        stream.Put_uint16 (1);

        stream.Put (s, 1);

        }

    SpoolString (stream,
                 fCategory,
                 kCategorySet,
                 3,
                 charSet);

    for (j = 0; j < fSupplementalCategories.Count (); j++)
        {

        SpoolString (stream,
                     fSupplementalCategories [j],
                     kSupplementalCategoriesSet,
                     32,
                     charSet);

        }

    for (j = 0; j < fKeywords.Count (); j++)
        {

        SpoolString (stream,
                     fKeywords [j],
                     kKeywordsSet,
                     64,
                     charSet);

        }

    SpoolString (stream,
                 fInstructions,
                 kSpecialInstructionsSet,
                 255,
                 charSet);

    if (fDateTimeCreated.IsValid ())
        {

        dng_string dateString = fDateTimeCreated.Encode_IPTC_Date ();

        if (dateString.NotEmpty ())
            {

            DNG_ASSERT (dateString.Length () == 8, "Wrong length IPTC date");

            stream.Put_uint16 (0x1C02);
            stream.Put_uint8  (kDateCreatedSet);

            stream.Put_uint16 (8);

            stream.Put (dateString.Get (), 8);

            }

        dng_string timeString = fDateTimeCreated.Encode_IPTC_Time ();

        if (timeString.NotEmpty ())
            {

            stream.Put_uint16 (0x1C02);
            stream.Put_uint8  (kTimeCreatedSet);

            stream.Put_uint16 ((uint16)timeString.Length ());

            stream.Put (timeString.Get (), timeString.Length ());

            }

        }

    if (fDigitalCreationDateTime.IsValid ())
        {

        dng_string dateString = fDigitalCreationDateTime.Encode_IPTC_Date ();

        if (dateString.NotEmpty ())
            {

            DNG_ASSERT (dateString.Length () == 8, "Wrong length IPTC date");

            stream.Put_uint16 (0x1C02);
            stream.Put_uint8  (kDigitalCreationDateSet);

            stream.Put_uint16 (8);

            stream.Put (dateString.Get (), 8);

            }

        dng_string timeString = fDigitalCreationDateTime.Encode_IPTC_Time ();

        if (timeString.NotEmpty ())
            {

            stream.Put_uint16 (0x1C02);
            stream.Put_uint8  (kDigitalCreationTimeSet);

            stream.Put_uint16 ((uint16)timeString.Length ());

            stream.Put (timeString.Get (), timeString.Length ());

            }

        }

    for (j = 0; j < fAuthors.Count (); j++)
        {

        SpoolString (stream,
                     fAuthors [j],
                     kBylineSet,
                     32,
                     charSet);

        }

    SpoolString (stream,
                 fAuthorsPosition,
                 kBylineTitleSet,
                 32,
                 charSet);

    SpoolString (stream,
                 fCity,
                 kCitySet,
                 32,
                 charSet);

    SpoolString (stream,
                 fLocation,
                 kSublocationSet,
                 32,
                 charSet);

    SpoolString (stream,
                 fState,
                 kProvinceStateSet,
                 32,
                 charSet);

    SpoolString (stream,
                 fCountryCode,
                 kCountryCodeSet,
                 3,
                 charSet);

    SpoolString (stream,
                 fCountry,
                 kCountryNameSet,
                 64,
                 charSet);

    SpoolString (stream,
                 fTransmissionReference,
                 kOriginalTransmissionReferenceSet,
                 32,
                 charSet);

    SpoolString (stream,
                 fHeadline,
                 kHeadlineSet,
                 255,
                 charSet);

    SpoolString (stream,
                 fCredit,
                 kCreditSet,
                 32,
                 charSet);

    SpoolString (stream,
                 fSource,
                 kSourceSet,
                 32,
                 charSet);

    SpoolString (stream,
                 fCopyrightNotice,
                 kCopyrightNoticeSet,
                 128,
                 charSet);

    SpoolString (stream,
                 fDescription,
                 kCaptionSet,
                 2000,
                 charSet);

    SpoolString (stream,
                 fDescriptionWriter,
                 kCaptionWriterSet,
                 32,
                 charSet);

    if (padForTIFF)
        {

        while (stream.Length () & 3)
            {
            stream.Put_uint8 (0);
            }

        }

    stream.Flush ();

    return stream.AsMemoryBlock (allocator);

    }

/*****************************************************************************/
