/*jslint nomen: true*/
/*global _, Color, MAPJS*/
MAPJS.defaultStyles = {};
MAPJS.layoutLinks = function (idea, visibleNodes) {
  'use strict';
  var result = {};
  _.each(idea.links, function (link) {
    if (visibleNodes[link.ideaIdFrom] && visibleNodes[link.ideaIdTo]) {
      result[link.ideaIdFrom + '_' + link.ideaIdTo] = {
        ideaIdFrom: link.ideaIdFrom,
        ideaIdTo: link.ideaIdTo,
        attr: _.clone(link.attr)
      };
      //todo - clone
    }
  });
  return result;
};
MAPJS.calculateFrame = function (nodes, margin) {
  'use strict';
  margin = margin || 0;
  var result = {
    top: _.min(nodes, function (node) {
      return node.y;
    }).y - margin,
    left: _.min(nodes, function (node) {
      return node.x;
    }).x - margin
  };
  result.width = margin + _.max(_.map(nodes, function (node) {
        return node.x + node.width;
      })) - result.left;
  result.height = margin + _.max(_.map(nodes, function (node) {
        return node.y + node.height;
      })) - result.top;
  return result;
};
MAPJS.contrastForeground = function (background) {
  'use strict';
  /*jslint newcap:true*/
  var luminosity = Color(background).luminosity();
  if (luminosity < 0.5) {
    return '#EEEEEE';
  }
  if (luminosity < 0.9) {
    return '#4F4F4F';
  }
  return '#000000';
};
MAPJS.Outline = function (topBorder, bottomBorder) {
  'use strict';
  var shiftBorder = function (border, deltaH) {
    return _.map(border, function (segment) {
      return {
        l: segment.l,
        h: segment.h + deltaH
      };
    });
  };
  this.initialHeight = function () {
    return this.bottom[0].h - this.top[0].h;
  };
  this.borders = function () {
    return _.pick(this, 'top', 'bottom');
  };
  this.spacingAbove = function (outline) {
    var i = 0, j = 0, result = 0, li = 0, lj = 0;
    while (i < this.bottom.length && j < outline.top.length) {
      result = Math.max(result, this.bottom[i].h - outline.top[j].h);
      if (li + this.bottom[i].l < lj + outline.top[j].l) {
        li += this.bottom[i].l;
        i += 1;
      } else if (li + this.bottom[i].l === lj + outline.top[j].l) {
        li += this.bottom[i].l;
        i += 1;
        lj += outline.top[j].l;
        j += 1;
      } else {
        lj += outline.top[j].l;
        j += 1;
      }
    }
    return result;
  };
  this.indent = function (horizontalIndent, margin) {
    if (!horizontalIndent) {
      return this;
    }
    var top = this.top.slice(),
        bottom = this.bottom.slice(),
        vertCenter = (bottom[0].h + top[0].h) / 2;
    top.unshift({h: vertCenter - margin / 2, l: horizontalIndent});
    bottom.unshift({h: vertCenter + margin / 2, l: horizontalIndent});
    return new MAPJS.Outline(top, bottom);
  };
  this.stackBelow = function (outline, margin) {
    var spacing = outline.spacingAbove(this),
        top = MAPJS.Outline.extendBorder(outline.top, shiftBorder(this.top, spacing + margin)),
        bottom = MAPJS.Outline.extendBorder(shiftBorder(this.bottom, spacing + margin), outline.bottom);
    return new MAPJS.Outline(
        top,
        bottom
    );
  };
  this.expand = function (initialTopHeight, initialBottomHeight) {
    var topAlignment = initialTopHeight - this.top[0].h,
        bottomAlignment = initialBottomHeight - this.bottom[0].h,
        top = shiftBorder(this.top, topAlignment),
        bottom = shiftBorder(this.bottom, bottomAlignment);
    return new MAPJS.Outline(
        top,
        bottom
    );
  };
  this.insertAtStart = function (dimensions, margin) {
    var alignment = 0, //-1 * this.top[0].h - suboutlineHeight * 0.5,
        topBorder = shiftBorder(this.top, alignment),
        bottomBorder = shiftBorder(this.bottom, alignment),
        easeIn = function (border) {
          border[0].l *= 0.5;
          border[1].l += border[0].l;
        };
    topBorder[0].l += margin;
    bottomBorder[0].l += margin;
    topBorder.unshift({h: -0.5 * dimensions.height, l: dimensions.width});
    bottomBorder.unshift({h: 0.5 * dimensions.height, l: dimensions.width});
    if (topBorder[0].h > topBorder[1].h) {
      easeIn(topBorder);
    }
    if (bottomBorder[0].h < bottomBorder[1].h) {
      easeIn(bottomBorder);
    }
    return new MAPJS.Outline(topBorder, bottomBorder);
  };
  this.top = topBorder.slice();
  this.bottom = bottomBorder.slice();
};
MAPJS.Outline.borderLength = function (border) {
  'use strict';
  return _.reduce(border, function (seed, el) {
    return seed + el.l;
  }, 0);
};
MAPJS.Outline.borderSegmentIndexAt = function (border, length) {
  'use strict';
  var l = 0, i = -1;
  while (l <= length) {
    i += 1;
    if (i >= border.length) {
      return -1;
    }
    l += border[i].l;
  }
  return i;
};
MAPJS.Outline.extendBorder = function (originalBorder, extension) {
  'use strict';
  var result = originalBorder.slice(),
      origLength = MAPJS.Outline.borderLength(originalBorder),
      i = MAPJS.Outline.borderSegmentIndexAt(extension, origLength),
      lengthToCut;
  if (i >= 0) {
    lengthToCut = MAPJS.Outline.borderLength(extension.slice(0, i + 1));
    result.push({h: extension[i].h, l: lengthToCut - origLength});
    result = result.concat(extension.slice(i + 1));
  }
  return result;
};
MAPJS.Tree = function (options) {
  'use strict';
  _.extend(this, options);
  this.toLayout = function (x, y, parentId) {
    x = x || 0;
    y = y || 0;
    var result = {
      nodes: {},
      connectors: {}
    }, self;
    self = _.pick(this, 'id', 'title', 'attr', 'width', 'height', 'level');
    self.attr.data = _.extend({}, self.attr.data);
    if (self.level === 1) {
      self.x = -0.5 * this.width;
      self.y = -0.5 * this.height;
    } else {
      self.x = x + this.deltaX || 0;
      self.y = y + this.deltaY || 0;
    }
    if (!self.attr.style) {
      self.attr.style = {};
    }
    result.nodes[this.id] = self;
    if (parentId !== undefined) {
      result.connectors[self.id] = {
        from: parentId,
        to: self.id
      };
    }
    if (this.subtrees) {
      this.subtrees.forEach(function (t) {
        var subLayout = t.toLayout(self.x, self.y, self.id);
        _.extend(result.nodes, subLayout.nodes);
        _.extend(result.connectors, subLayout.connectors);
      });
    }
    return result;
  };
};
MAPJS.Outline.fromDimensions = function (dimensions) {
  'use strict';
  return new MAPJS.Outline([{
    h: -0.5 * dimensions.height,
    l: dimensions.width
  }], [{
    h: 0.5 * dimensions.height,
    l: dimensions.width
  }]);
};
MAPJS.calculateTree = function (content, dimensionProvider, margin, rankAndParentPredicate, level) {
  'use strict';
  var options = {
        id: content.id,
        title: content.title,
        attr: $.extend({}, content.attr, {hasChildren: !_.isEmpty(content.ideas)}),
        deltaY: 0,
        deltaX: 0,
        level: level || 1
      },
      setVerticalSpacing = function (treeArray, dy) {
        var i,
            tree,
            oldSpacing,
            newSpacing,
            oldPositions = _.map(treeArray, function (t) {
              return _.pick(t, 'deltaX', 'deltaY');
            }),
            referenceTree,
            alignment;
        for (i = 0; i < treeArray.length; i += 1) {
          tree = treeArray[i];
          if (tree.attr && tree.attr.position) {
            tree.deltaY = tree.attr.position[1];
            if (referenceTree === undefined || tree.attr.position[2] > treeArray[referenceTree].attr.position[2]) {
              referenceTree = i;
            }
          } else {
            tree.deltaY += dy;
          }
          if (i > 0) {
            oldSpacing = oldPositions[i].deltaY - oldPositions[i - 1].deltaY;
            newSpacing = treeArray[i].deltaY - treeArray[i - 1].deltaY;
            if (newSpacing < oldSpacing) {
              tree.deltaY += oldSpacing - newSpacing;
            }
          }
        }
        alignment = referenceTree && (treeArray[referenceTree].attr.position[1] - treeArray[referenceTree].deltaY);
        if (alignment) {
          for (i = 0; i < treeArray.length; i += 1) {
            treeArray[i].deltaY += alignment;
          }
        }
      },
      shouldIncludeSubIdeas = function () {
        return !(_.isEmpty(content.ideas) || (content.attr && content.attr.collapsed));
      },
      includedSubIdeaKeys = function () {
        var allRanks = _.map(_.keys(content.ideas), parseFloat),
            includedRanks = rankAndParentPredicate ? _.filter(allRanks, function (rank) {
              return rankAndParentPredicate(rank, content);
            }) : allRanks;
        return _.sortBy(includedRanks, Math.abs);
      },
      includedSubIdeas = function () {
        var result = [];
        _.each(includedSubIdeaKeys(), function (key) {
          result.push(content.ideas[key]);
        });
        return result;
      },
      nodeDimensions = dimensionProvider(content, options.level),
      appendSubtrees = function (subtrees) {
        var suboutline, deltaHeight, subtreePosition, horizontal, treeOutline;
        _.each(subtrees, function (subtree) {
          subtree.deltaX = nodeDimensions.width + margin;
          subtreePosition = subtree.attr && subtree.attr.position && subtree.attr.position[0];
          if (subtreePosition && subtreePosition > subtree.deltaX) {
            horizontal = subtreePosition - subtree.deltaX;
            subtree.deltaX = subtreePosition;
          } else {
            horizontal = 0;
          }
          if (!suboutline) {
            suboutline = subtree.outline.indent(horizontal, margin);
          } else {
            treeOutline = subtree.outline.indent(horizontal, margin);
            deltaHeight = treeOutline.initialHeight();
            suboutline = treeOutline.stackBelow(suboutline, margin);
            subtree.deltaY = suboutline.initialHeight() - deltaHeight / 2 - subtree.height / 2;
          }
        });
        if (subtrees && subtrees.length) {
          setVerticalSpacing(subtrees, 0.5 * (nodeDimensions.height - suboutline.initialHeight()));
          suboutline = suboutline.expand(
              subtrees[0].deltaY - nodeDimensions.height * 0.5,
              subtrees[subtrees.length - 1].deltaY + subtrees[subtrees.length - 1].height - nodeDimensions.height * 0.5
          );
        }
        options.outline = suboutline.insertAtStart(nodeDimensions, margin);
      };
  _.extend(options, nodeDimensions);
  options.outline = new MAPJS.Outline.fromDimensions(nodeDimensions);
  if (shouldIncludeSubIdeas()) {
    options.subtrees = _.map(includedSubIdeas(), function (i) {
      return MAPJS.calculateTree(i, dimensionProvider, margin, rankAndParentPredicate, options.level + 1);
    });
    if (!_.isEmpty(options.subtrees)) {
      appendSubtrees(options.subtrees);
    }
  }
  return new MAPJS.Tree(options);
};

MAPJS.calculateLayout = function (idea, dimensionProvider, margin,layoutPredicate) {
  'use strict';
  var positiveTree, negativeTree, layout, negativeLayout, positive, negative,
      setDefaultStyles = function (nodes) {
        _.each(nodes, function (node) {
          node.attr = node.attr || {};
          node.attr.style = _.extend({}, MAPJS.defaultStyles[(node.level === 1) ? 'root' : 'nonRoot'], node.attr.style);
        });
      };
  positive = layoutPredicate(idea, true);
  negative = layoutPredicate(idea, false);
  margin = margin || 20;
  positiveTree = MAPJS.calculateTree(idea, dimensionProvider, margin, positive);
  negativeTree = MAPJS.calculateTree(idea, dimensionProvider, margin, negative);
  layout = positiveTree.toLayout();
  negativeLayout = negativeTree.toLayout();
  _.each(negativeLayout.nodes, function (n) {
    n.x = -1 * n.x - n.width;
  });
  _.extend(negativeLayout.nodes, layout.nodes);
  _.extend(negativeLayout.connectors, layout.connectors);
  setDefaultStyles(negativeLayout.nodes);
  negativeLayout.links = MAPJS.layoutLinks(idea, negativeLayout.nodes);
  negativeLayout.rootNodeId = idea.id;
  return negativeLayout;
};