(function() {
  'use strict';

  angular
    .module('orchestra.common')
    .directive('orchestraQuill', ['$http', '$timeout', orchestraQuill]);

  function orchestraQuill($http, $timeout) {
    return {
      restrict: 'E',
      scope: {
        'data': '=',
        'imagePrefix': '=?',
        'readonly': '=',
        'uploadLimitMb': '=?',
      },
      link: function(scope, el, attr) {
        // Suggested limit for image upload size is 5 MB.
        scope.uploadLimitMb = scope.uploadLimitMb || 5;

        // Default image prefix is an empty string.
        scope.imagePrefix = scope.imagePrefix || '';

        // Containers within directive template for Quill editor
        var editorContainer = el.find('.orchestra-quill-editor').get(0);
        var toolbarContainer = el.find('.orchestra-quill-toolbar').get(0);

        scope.editor = new Quill(editorContainer, {
          modules: {
            'toolbar': {
              container: toolbarContainer
            },
            // Image tooltip removed in favor of file selector dialog
            'link-tooltip': true,
          },
          theme: 'snow'
        });

        // Set up two-way link between quill and parent scope attribute passed
        // in by directive caller
        scope.$watch('data', function(now, before) {
          if (scope.data && scope.data != scope.editor.getHTML()) {
            scope.editor.setHTML(scope.data);
          }
        });

        // Editor set to read-only upon initialization.
        if (scope.readonly) {
          scope.editor.editor.disable();
          toolbarContainer.remove();
          return;
        }

        scope.editor.on('text-change', function() {
          // Set the focus outside the $digest block.
          // Taken from https://docs.angularjs.org/error/$rootScope/inprog?p0=$digest.
          $timeout(function() {
            scope.data = scope.editor.getHTML();
            scope.$apply();
          }, 0, false);
        });

        // Upload image via the toolbar button
        scope.fileSelector = document.createElement('input');
        scope.fileSelector.setAttribute('type', 'file');
        scope.fileSelector.setAttribute('accept', 'image/*');
        var imageSelector = el.find('.orchestra-quill-toolbar .ql-image').get(0);
        scope.fileSelector.onchange = function(e) {
          // Focus the editor so we can get the current selection.
          scope.editor.focus();
          var files = scope.fileSelector.files;
          for (var i = files.length - 1; i >= 0; i--) {
            if (files[i] !== null) {
              uploadImage(files[i], scope.editor.getSelection());
            }
          }
        };
        imageSelector.onclick = function() {
          scope.fileSelector.click();
          return false;
        };

        // TODO(jrbotros): move paste/drop functionality into a quilljs fork

        // Upload image by pasting in copied image data
        editorContainer.addEventListener('paste', pasteImage, true);

        function pasteImage(e) {
          if (e.clipboardData && e.clipboardData.items) {
            var copiedData = e.clipboardData.items[0];
            var imageFile = copiedData.getAsFile();
            uploadImage(imageFile, scope.editor.getSelection(), e);
          }
        }

        // Upload image by drag and drop
        editorContainer.addEventListener('drop', dropImage, true);

        function dropImage(e) {
          var files = e.dataTransfer.files;
          for (var i = files.length - 1; i >= 0; i--) {
            var file = files[i];
            var dropOffset = getDropIndexOffset(e);
            uploadImage(file, {
              'start': dropOffset,
              'end': dropOffset
            }, e);
          }
        }

        function getDropIndexOffset(dropEvent) {
          /**
           * Given a drop event, calculate the index offset within the
           * Quill editor by:
           *   - determining the character offset of the drop within the inner node
           *   - finding all editor leaf nodes (reimplements some Quill functionality)
           *   - calculating the index offset from the leaf node lengths and inner drop node offset
           * We plan to push this functionality into Quill in the future.
           */
          function getDropCharOffset(x, y) {
            // Helper function for getting the x-y character offset of a drop
            // event in a contenteditable field.
            // Modified from http://stackoverflow.com/a/10659990.
            var range;
            // Standards-based way; implemented only in Firefox
            if (document.caretPositionFromPoint) {
              var pos = document.caretPositionFromPoint(x, y);
              return {
                'offset': pos.offset,
                'node': pos.offsetNode
              };
            } else if (document.caretRangeFromPoint) {
              // Webkit
              range = document.caretRangeFromPoint(x, y);
              return {
                'offset': range.startOffset,
                'node': range.startContainer
              };
            } else if (document.body.createTextRange) {
              // IE doesn't natively support retrieving the character offset, so
              // insert image at end of text
              // TODO(jrbotros): rewrite with https://github.com/timdown/rangy
              return;
            }
          }

          function getAllChildNodes(parent) {
            // Returns in-order list of all child nodes of `parent`
            var bfsStack = [parent];
            var nodes = [];
            while (bfsStack.length) {
              var currentNode = bfsStack.pop();
              if (currentNode.childNodes) {
                // Concat with copy of child NodeList; can't use pop operation
                // on original
                bfsStack = bfsStack.concat(Array.prototype.slice.call(currentNode.childNodes));
              }
              nodes.push(currentNode);
            }
            return nodes.reverse();
          }

          // http://stackoverflow.com/a/22289650
          function getLeafNodes(parent) {
            // Returns in-order list of all Quill leaf nodes of `parent`
            var textNodeType = 3;
            var nodes = getAllChildNodes(parent);
            var leafNodes = nodes.filter(function(elem) {
              return elem.nodeType === textNodeType || !elem.firstChild;
            });
            return leafNodes;
          }

          var caretPosition = getDropCharOffset(dropEvent.clientX, dropEvent.clientY);
          if (!caretPosition) {
            return scope.editor.getLength();
          }
          var leaves = getLeafNodes(editorContainer.getElementsByClassName('ql-editor')[0]);
          var opIndex = leaves.indexOf(caretPosition.node);
          var contents = scope.editor.getContents();
          contents.ops = contents.ops.slice(0, opIndex);
          return contents.length() + caretPosition.offset;
        }

        // Post image data to an API endpoint that returns an image URL, then
        // adding it to the editor (replacing the given character range)
        function uploadImage(file, range, e) {
          var uploadAPIEndpoint = '/orchestra/api/interface/upload_image/';
          var supportedTypes = ['image/jpeg', 'image/png', 'image/gif'];

          if (supportedTypes.indexOf(file.type) === -1) {
            alert('Files type ' + file.type + ' not supported.');
            return;
          } else if (e) {
            // Cancel default browser action only if file is actually an image
            e.preventDefault();
          }

          // If nothing is selected in the editor, append the image to its end
          if (range === null) {
            var endIndex = scope.editor.getLength();
            range = {
              'start': endIndex,
              'end': endIndex
            };
          }

          var reader = new FileReader();
          reader.onload = function(e) {
            var rawData = e.target.result;
            // Remove prepended image type from data string
            var imageData = rawData.substring(rawData.indexOf(',') + 1, rawData.length);

            // Calculate data size of b64-encoded string
            var imageSize = imageData.length * 3 / 4;

            if (imageSize > scope.uploadLimitMb * Math.pow(10, 6)) {
              alert('Files larger than ' + scope.uploadLimitMb + 'MB cannot be uploaded');
              return;
            }
            // Post image data to API; response should contain the uploaded image url
            $http.post(uploadAPIEndpoint, {
                'image_data': imageData,
                'image_type': file.type,
                'prefix': scope.imagePrefix
              })
              .then(function(response, status, headers, config) {
                // Replace selected range with new image
                scope.editor.deleteText(range);
                scope.editor.insertEmbed(range.start, 'image', response.data.url, 'user');
              });
          };
          reader.readAsDataURL(file);
        }
      },
      templateUrl: $static('/static/orchestra/common/components/quill/partials/quill.html')
    };
  }
})();
