// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

var exif = {
  verbose: false,

  messageHandlers: {
    "init": function() {
      this.log('thumbnailer initialized');
    },

    "get-exif": function(fileURL) {
      this.processOneFile(fileURL, function callback(metadata) {
          postMessage({verb: 'give-exif',
                       arguments: [fileURL, metadata]});
      });
    },
  },

  processOneFile: function(fileURL, callback) {
    var self = this;
    var currentStep = -1;

    function nextStep(var_args) {
      self.vlog('nextStep: ' + steps[currentStep + 1].name);
      steps[++currentStep].apply(self, arguments);
    }

    function onError(err) {
      self.vlog('Error processing: ' + fileURL + ': step: ' +
                steps[currentStep].name + ": " + err);

      postMessage({verb: 'give-exif-error',
                   arguments: [fileURL, steps[currentStep].name, err]});
    }

    var steps =
    [ // Step one, turn the url into an entry.
      function getEntry() {
        webkitResolveLocalFileSystemURL(fileURL,
                                        function(entry) { nextStep(entry) },
                                        onError);
      },

      // Step two, turn the entry into a file.
      function getFile(entry) {
        entry.file(function(file) { nextStep(file) }, onError);
      },

      // Step three, read the file header into a byte array.
      function readHeader(file) {
        var reader = new FileReader(file.webkitSlice(0, 1024));
        reader.onerror = onError;
        reader.onload = function(event) { nextStep(file, reader.result) };
        reader.readAsArrayBuffer(file);
      },

      // Step four, find the exif marker and read all exif data.
      function findExif(file, buf) {
        var br = new exif.BufferReader(buf);
        var mark = br.readMark();
        if (mark != exif.MARK_SOI)
          return onError('Invalid file header: ' + mark.toString(16));

        while (true) {
          if (mark == exif.MARK_SOS || br.eof()) {
            return onError('Unable to find EXIF marker');
          }

          mark = br.readMark();
          if (mark == exif.MARK_EXIF) {
            var length = br.readMarkLength();

            // Offsets inside the EXIF block are based after this bit of
            // magic, so we verify and discard it here, before exif parsing,
            // to make offset calculations simpler.
            var magic = br.readString(6);
            if (magic != 'Exif\0\0')
              return onError('Invalid EXIF magic: ' + magic.toString(16));

            var pos = br.tell();
            var reader = new FileReader();
            reader.onerror = onError;
            reader.onload = function(event) { nextStep(file, reader.result) };
            reader.readAsArrayBuffer(file.webkitSlice(pos, pos + length - 6));
            return;
          }

          br.skipMarkData();
        }
      },

      // Step five, parse the exif data.
      function parseExif(file, buf) {
        var br = new exif.BufferReader(buf);
        var order = br.readScalar(2);
        if (order == exif.ALIGN_LITTLE) {
          br.setByteOrder(exif.BufferReader.LITTLE_ENDIAN);
        } else if (order != exif.ALIGN_BIG) {
          return onError('Invalid alignment value: ' + order.toString(16));
        }

        var tag = br.readScalar(2);
        if (tag != exif.TAG_TIFF)
          return onError('Invalid TIFF tag: ' + tag.toString(16));

        var tags = {};
        var directoryOffset = br.readScalar(4);

        while (directoryOffset) {
          br.seek(directoryOffset);
          var entryCount = br.readScalar(2);
          for (var i = 0; i < entryCount; i++) {
            var tag = tags[br.readScalar(2)] = {};
            tag.format = br.readScalar(2);
            tag.componentCount = br.readScalar(4);
            tag.value = br.readScalar(4);
          };

          directoryOffset = br.readScalar(4);
        }

        var metadata = { rawTags: tags };

        if (exif.TAG_JPG_THUMB_OFFSET in tags &&
            exif.TAG_JPG_THUMB_LENGTH in tags) {
          br.seek(tags[exif.TAG_JPG_THUMB_OFFSET].value);
          var b64 = br.readBase64(tags[exif.TAG_JPG_THUMB_LENGTH].value);
          metadata.thumbnailURL = 'data:image/jpeg;base64,' + b64;
        } else {
          self.vlog('Image has EXIF data, but no JPG thumbnail.');
        }

        if (exif.TAG_EXIF_IMAGE_WIDTH in tags)
          metadata.exifImageWidth = tags[exif.TAG_IMAGE_WIDTH];

        if (exif.TAG_EXIF_IMAGE_HEIGHT in tags)
          metadata.exifImageHeight = tags[exif.TAG_IMAGE_HEIGHT];

        nextStep(metadata);
      },

      // Step six, we're done.
      callback
    ];

    nextStep();
  },

  onMessage: function(event) {
    var data = event.data;

    if (this.messageHandlers.hasOwnProperty(data.verb)) {
      //this.log('dispatching: ' + data.verb + ': ' + data.arguments);
      this.messageHandlers[data.verb].apply(this, data.arguments);
    } else {
      this.log('Unknown message from client: ' + data.verb, data);
    }
  },

  log: function(var_args) {
    var ary = Array.apply(null, arguments);
    postMessage({verb: 'log', arguments: ary});
  },

  vlog: function(var_args) {
    if (this.verbose)
      this.log.apply(this, arguments);
  }
};

exif.MARK_SOI = 0xffd8;  // Start of image data.
exif.MARK_SOS = 0xffda;  // Start of "stream" (the actual image data).
exif.MARK_EXIF = 0xffe1;  // Start of exif block.

exif.ALIGN_LITTLE = 0x4949;  // Indicates little endian alignment of exif data.
exif.ALIGN_BIG = 0x4d4d;  // Indicates big endian alignment of exif data.

exif.TAG_TIFF = 0x002a;  // First tag in the exif data.
exif.TAG_JPG_THUMB_OFFSET = 0x0201;
exif.TAG_JPG_THUMB_LENGTH = 0x0202;
exif.TAG_EXIF_IMAGE_WIDTH = 0xa002;
exif.TAG_EXIF_IMAGE_HEIGHT = 0xa003;

exif.BufferReader = function(buf) {
  this.buf_ = buf;
  this.ary_ = new Uint8Array(buf);
  this.pos_ = 0;
  this.setByteOrder(exif.BufferReader.BIG_ENDIAN);
};

exif.BufferReader.LITTLE_ENDIAN = 0;  // Intel, 0x1234 is [0x34, 0x12]
exif.BufferReader.BIG_ENDIAN = 1;  // Motorola, 0x002a is [0x12, 0x34]

exif.BufferReader.prototype = {
  setByteOrder: function(order) {
    this.order_ = order;
    if (order == exif.BufferReader.LITTLE_ENDIAN) {
      this.readScalar = this.readLittle;
    } else {
      this.readScalar = this.readBig;
    }
  },

  eof: function() {
    return this.pos_ >= this.ary_.length;
  },

  readScalar: null,  // Either readLittle or readBig, according to byte order.

  /**
   * Big endian read.  Most significant bytes come first.
   */
  readBig: function(width) {
    var rv = 0;
    switch(width) {
      case 4:
        rv = this.ary_[this.pos_++] << 24;
      case 3:
        rv |= this.ary_[this.pos_++] << 16;
      case 2:
        rv |= this.ary_[this.pos_++] << 8;
      case 1:
        rv |= this.ary_[this.pos_++];
    }

    return rv;
  },

  /**
   * Little endian read.  Least significant bytes come first.
   */
  readLittle: function(width) {
    var rv = 0;
    switch(width) {
      case 4:
        rv = this.ary_[this.pos_ + 3] << 24;
      case 3:
        rv |= this.ary_[this.pos_ + 2] << 16;
      case 2:
        rv |= this.ary_[this.pos_+ 1] << 8;
      case 1:
        rv |= this.ary_[this.pos_];
    }

    this.pos_ += width;
    return rv;
  },

  readString: function(length) {
    var chars = [];
    for (var i = 0; i < length; i++) {
      chars[i] = String.fromCharCode(this.ary_[this.pos_++]);
    }

    return chars.join('');
  },

  base64Alphabet_: ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
                    'abcdefghijklmnopqrstuvwxyz' +
                    '0123456789+/').split(''),

  readBase64: function(length) {
    var rv = [];
    var chars = [];
    var padding = 0;

    for (var i = 0; i < length; /* incremented inside */) {
      var bits = this.ary_[this.pos_ + i++] << 16;

      if (i < length) {
        bits |= this.ary_[this.pos_ + i++] << 8;

        if (i < length) {
          bits |= this.ary_[this.pos_ + i++];
        } else {
          padding = 1;
        }
      } else {
        padding = 2;
      }

      chars[3] = this.base64Alphabet_[bits & 63];
      chars[2] = this.base64Alphabet_[(bits >> 6) & 63];
      chars[1] = this.base64Alphabet_[(bits >> 12) & 63];
      chars[0] = this.base64Alphabet_[(bits >> 18) & 63];

      rv.push.apply(rv, chars);
    }

    this.pos_ += i;

    if (padding > 0)
      chars[chars.length - 1] = '=';
    if (padding > 1)
      chars[chars.length - 2] = '=';

    return rv.join('');
  },

  readMark: function() {
    return this.readScalar(2);
  },

  readMarkLength: function() {
    // Length includes the 2 bytes used to store the length.
    return this.readScalar(2) - 2;
  },

  readMarkData: function(opt_arrayConstructor) {
    var arrayConstructor = opt_arrayConstructor || Uint8Array;

    var length = this.readMarkLength();
    var slice = new arrayConstructor(this.buf_, this.pos_, length);
    this.pos_ += length;

    return slice;
  },

  skipMarkData: function() {
    this.skip(this.readMarkLength());
  },

  seek: function(pos) {
    this.pos_ = pos;
  },

  skip: function(count) {
    this.pos_ += count;
  },

  tell: function() {
    return this.pos_;
  }
};

var onmessage = exif.onMessage.bind(exif);