/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */

'use strict';

var consoleUtils = (function() {
  var consoleTimer = {};

  var obj = {};
  obj.time = function(name) {
    consoleTimer[name] = Date.now();
  };
  obj.timeEnd = function(name) {
    var time = consoleTimer[name];
    if (time == null) {
      throw 'Unkown timer name ' + name;
    }
    console.log('Timer:', name, Date.now() - time);
  };

  return obj;
})();

function FontWorker() {
  this.worker = new Worker('../worker/font.js');
  this.fontsWaiting = 0;
  this.fontsWaitingCallbacks = [];

  // Listen to the WebWorker for data and call actionHandler on it.
  this.worker.onmessage = function(event) {
    var data = event.data;
    var actionHandler = this.actionHandler;
    if (data.action in actionHandler) {
      actionHandler[data.action].call(this, data.data);
    } else {
      throw 'Unkown action from worker: ' + data.action;
    }
  }.bind(this);

  this.$handleFontLoadedCallback = this.handleFontLoadedCallback.bind(this);
}

FontWorker.prototype = {
  handleFontLoadedCallback: function() {
    // Decrease the number of fonts wainting to be loaded.
    this.fontsWaiting--;
    // If all fonts are available now, then call all the callbacks.
    if (this.fontsWaiting == 0) {
      var callbacks = this.fontsWaitingCallbacks;
      for (var i = 0; i < callbacks.length; i++) {
        callbacks[i]();
      }
      this.fontsWaitingCallbacks.length = 0;
    }
  },

  actionHandler: {
    'log': function(data) {
      console.log.apply(console, data);
    },

    'fonts': function(data) {
      // console.log("got processed fonts from worker", Object.keys(data));
      for (var name in data) {
        // Update the encoding property.
        var font = Fonts.lookup(name);
        font.properties = {
          encoding: data[name].encoding
        };

        // Call `Font.prototype.bindDOM` to make the font get loaded
        // on the page.
        Font.prototype.bindDOM.call(
          font,
          data[name].str,
          // IsLoadedCallback.
          this.$handleFontLoadedCallback
        );
      }
    }
  },

  ensureFonts: function(data, callback) {
    var font;
    var notLoaded = [];
    for (var i = 0; i < data.length; i++) {
      font = data[i];
      if (Fonts[font.name]) {
        continue;
      }

      // Register the font but don't pass in any real data. The idea is to
      // store as less data as possible to reduce memory usage.
      Fonts.registerFont(font.name, Object.create(null), Object.create(null));

      // Mark this font to be handled later.
      notLoaded.push(font);
      // Increate the number of fonts to wait for.
      this.fontsWaiting++;
    }

    consoleUtils.time('ensureFonts');
    // If there are fonts, that need to get loaded, tell the FontWorker to get
    // started and push the callback on the waiting-callback-stack.
    if (notLoaded.length != 0) {
      console.log('fonts -> FontWorker');
      // Send the worker the fonts to work on.
      this.worker.postMessage({
        action: 'fonts',
        data: notLoaded
      });
      if (callback) {
        this.fontsWaitingCallbacks.push(callback);
      }
    }
    // All fonts are present? Well, then just call the callback if there is one.
    else {
      if (callback) {
        callback();
      }
    }
  }
};

function WorkerPDFDoc(canvas) {
  var timer = null;

  this.ctx = canvas.getContext('2d');
  this.canvas = canvas;
  this.worker = new Worker('../worker/pdf.js');
  this.fontWorker = new FontWorker();
  this.waitingForFonts = false;
  this.waitingForFontsCallback = [];

  this.numPage = 1;
  this.numPages = null;

  var imagesList = {};
  var canvasList = {
    0: canvas
  };
  var patternList = {};
  var gradient;

  var currentX = 0;
  var currentXStack = [];

  var ctxSpecial = {
    '$setCurrentX': function(value) {
      currentX = value;
    },

    '$addCurrentX': function(value) {
      currentX += value;
    },

    '$saveCurrentX': function() {
      currentXStack.push(currentX);
    },

    '$restoreCurrentX': function() {
      currentX = currentXStack.pop();
    },

    '$showText': function(y, text) {
      text = Fonts.charsToUnicode(text);
      this.translate(currentX, -1 * y);
      this.fillText(text, 0, 0);
      currentX += this.measureText(text).width;
    },

    '$putImageData': function(imageData, x, y) {
      var imgData = this.getImageData(0, 0,
        imageData.width, imageData.height);

      // Store the .data property to avaid property lookups.
      var imageRealData = imageData.data;
      var imgRealData = imgData.data;

      // Copy over the imageData.
      var len = imageRealData.length;
      while (len--)
        imgRealData[len] = imageRealData[len];

      this.putImageData(imgData, x, y);
    },

    '$drawImage': function(id, x, y, sx, sy, swidth, sheight) {
      var image = imagesList[id];
      if (!image) {
        throw 'Image not found: ' + id;
      }
      this.drawImage(image, x, y, image.width, image.height,
        sx, sy, swidth, sheight);
    },

    '$drawCanvas': function(id, x, y, sx, sy, swidth, sheight) {
      var canvas = canvasList[id];
      if (!canvas) {
        throw 'Canvas not found';
      }
      if (sheight != null) {
        this.drawImage(canvas, x, y, canvas.width, canvas.height,
          sx, sy, swidth, sheight);
      } else {
        this.drawImage(canvas, x, y, canvas.width, canvas.height);
      }
    },

    '$createLinearGradient': function(x0, y0, x1, y1) {
      gradient = this.createLinearGradient(x0, y0, x1, y1);
    },

    '$createPatternFromCanvas': function(patternId, canvasId, kind) {
      var canvas = canvasList[canvasId];
      if (!canvas) {
        throw 'Canvas not found';
      }
      patternList[patternId] = this.createPattern(canvas, kind);
    },

    '$addColorStop': function(i, rgba) {
      gradient.addColorStop(i, rgba);
    },

    '$fillStyleGradient': function() {
      this.fillStyle = gradient;
    },

    '$fillStylePattern': function(id) {
      var pattern = patternList[id];
      if (!pattern) {
        throw 'Pattern not found';
      }
      this.fillStyle = pattern;
    },

    '$strokeStyleGradient': function() {
      this.strokeStyle = gradient;
    },

    '$strokeStylePattern': function(id) {
      var pattern = patternList[id];
      if (!pattern) {
        throw 'Pattern not found';
      }
      this.strokeStyle = pattern;
    },

    '$setFont': function(name, size) {
      this.font = size + 'px "' + name + '"';
      Fonts.setActive(name, size);
    }
  };

  function renderProxyCanvas(canvas, cmdQueue) {
    var ctx = canvas.getContext('2d');
    var cmdQueueLength = cmdQueue.length;
    for (var i = 0; i < cmdQueueLength; i++) {
      var opp = cmdQueue[i];
      if (opp[0] == '$') {
        ctx[opp[1]] = opp[2];
      } else if (opp[0] in ctxSpecial) {
        ctxSpecial[opp[0]].apply(ctx, opp[1]);
      } else {
        ctx[opp[0]].apply(ctx, opp[1]);
      }
    }
  }

  /**
  * Functions to handle data sent by the WebWorker.
  */
  var actionHandler = {
    'log': function(data) {
      console.log.apply(console, data);
    },

    'pdf_num_pages': function(data) {
      this.numPages = parseInt(data, 10);
      if (this.loadCallback) {
        this.loadCallback();
      }
    },

    'font': function(data) {
      var base64 = window.btoa(data.raw);

      // Add the @font-face rule to the document
      var url = 'url(data:' + data.mimetype + ';base64,' + base64 + ');';
      var rule = ("@font-face { font-family:'" + data.fontName +
                  "';src:" + url + '}');
      var styleSheet = document.styleSheets[0];
      styleSheet.insertRule(rule, styleSheet.cssRules.length);

      // Just adding the font-face to the DOM doesn't make it load. It
      // seems it's loaded once Gecko notices it's used. Therefore,
      // add a div on the page using the loaded font.
      var div = document.createElement('div');
      var style = 'font-family:"' + data.fontName +
        '";position: absolute;top:-99999;left:-99999;z-index:-99999';
      div.setAttribute('style', style);
      document.body.appendChild(div);
    },

    'setup_page': function(data) {
      var size = data.split(',');
      var canvas = this.canvas, ctx = this.ctx;
      canvas.width = parseInt(size[0], 10);
      canvas.height = parseInt(size[1], 10);
    },

    'fonts': function(data) {
      this.waitingForFonts = true;
      this.fontWorker.ensureFonts(data, function() {
        this.waitingForFonts = false;
        var callbacks = this.waitingForFontsCallback;
        for (var i = 0; i < callbacks.length; i++) {
          callbacks[i]();
        }
        this.waitingForFontsCallback.length = 0;
      }.bind(this));
    },

    'jpeg_stream': function(data) {
      var img = new Image();
      img.src = 'data:image/jpeg;base64,' + window.btoa(data.raw);
      imagesList[data.id] = img;
    },

    'canvas_proxy_cmd_queue': function(data) {
        var id = data.id;
        var cmdQueue = data.cmdQueue;

        // Check if there is already a canvas with the given id. If not,
        // create a new canvas.
        if (!canvasList[id]) {
          var newCanvas = document.createElement('canvas');
          newCanvas.width = data.width;
          newCanvas.height = data.height;
          canvasList[id] = newCanvas;
        }

        var renderData = function() {
          if (id == 0) {
            consoleUtils.time('main canvas rendering');
            var ctx = this.ctx;
            ctx.save();
            ctx.fillStyle = 'rgb(255, 255, 255)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.restore();
          }
          renderProxyCanvas(canvasList[id], cmdQueue);
          if (id == 0) {
            consoleUtils.timeEnd('main canvas rendering');
            consoleUtils.timeEnd('>>> total page display time:');
          }
        }.bind(this);

        if (this.waitingForFonts) {
          if (id == 0) {
            console.log('want to render, but not all fonts are there', id);
            this.waitingForFontsCallback.push(renderData);
          } else {
            // console.log("assume canvas doesn't have fonts", id);
            renderData();
          }
        } else {
          renderData();
        }
    }
  };

  // Listen to the WebWorker for data and call actionHandler on it.
  this.worker.addEventListener('message', function(event) {
    var data = event.data;
    if (data.action in actionHandler) {
      actionHandler[data.action].call(this, data.data);
    } else {
      throw 'Unkown action from worker: ' + data.action;
    }
  }.bind(this));
}

WorkerPDFDoc.prototype = {
  open: function(url, callback) {
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.mozResponseType = req.responseType = 'arraybuffer';
    req.expected = (document.URL.indexOf('file:') == 0) ? 0 : 200;
    req.onreadystatechange = function() {
      if (req.readyState == 4 && req.status == req.expected) {
        var data = req.mozResponseArrayBuffer || req.mozResponse ||
        req.responseArrayBuffer || req.response;

        this.loadCallback = callback;
        this.worker.postMessage(data);
        this.showPage(this.numPage);
      }
    }.bind(this);
    req.send(null);
  },

  showPage: function(numPage) {
    this.numPage = parseInt(numPage, 10);
    console.log('=== start rendering page ' + numPage + ' ===');
    consoleUtils.time('>>> total page display time:');
    this.worker.postMessage(numPage);
    if (this.onChangePage) {
      this.onChangePage(numPage);
    }
  },

  nextPage: function() {
    if (this.numPage != this.numPages)
      this.showPage(++this.numPage);
  },

  prevPage: function() {
    if (this.numPage != 1)
      this.showPage(--this.numPage);
  }
};