#!/usr/bin/env python
#
# Copyright 2012 Johannes 'josch' Schauer <j.schauer@email.de>
#
# This file is part of Sisyphus.
#
# Sisyphus is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Sisyphus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Sisyphus.  If not, see <http://www.gnu.org/licenses/>.

# inspired by Jake Gordon's bin-packing algorithm
# https://github.com/jakesgordon/bin-packing/blob/master/js/packer.js
# http://codeincomplete.com/posts/2011/5/7/bin_packing/
# Jake Gordon was inspired by Jim Scott's solution on how to pack lightmaps
# http://www.blackpawn.com/texts/lightmaps/default.html

import svg
import random

# rounds to the next biggest even number
def roundeven(num):
    return (num+1)/2*2

def arrange_in_layer(abin, pwidth, pheight):
    # articles are longer than wider
    # default rotation: width: x-direction
    #                   height:  y-direction

    layer = list()
    rest = list()
    root = {'x': 0, 'y': 0,
            'width': pwidth, 'height': pheight,
            'article': None,
            'down': None,
            'right': None}

    # traverse the tree until a node is found that is big enough for article
    # with size width x height and return this node or None if not found
    def find_node(root, width, height):
        if root is None:
            return None
        elif root['article']:
            return (find_node(root['right'], width, height)
                 or find_node(root['down'], width, height))
        elif width <= root['width'] and height <= root['height']:
            return root
        else:
            return None

    # after finding a node where an article fits, put article into the node and
    # create childnodes
    def split_node(node, width, height, article):
        node['article'] = article
        node['article']['x'] = node['x']
        node['article']['y'] = node['y']
        if node['width'] > 0 and node['height']-height > 0:
            node['down'] = {'x': node['x'], 'y': node['y']+height,
                            'width': node['width'],
                            'height': node['height']-height,
                            'article': None,
                            'down': None,
                            'right': None}
        else:
            node['down'] = None
        if node['width']-width > 0 and height > 0:
            node['right'] = {'x': node['x']+width, 'y': node['y'],
                             'width': node['width']-width,
                             'height': height,
                             'article': None,
                             'down': None,
                             'right': None}
        else:
            node['right'] = None
        return node

    # for each article in abin, check and place article at a node. If it doesnt
    # fit, try to rotate. If it still doesnt fit, append to rest
    for article in abin:
        # output format only accepts integer positions, round article sizes up
        # to even numbers
        owidth, oheight = article['width'], article['height']
        width, height = roundeven(owidth), roundeven(oheight)

        node = find_node(root, width, height)
        if (node):
            node = split_node(node, width, height, article)
        else:
            # rotate article
            article['width'], article['height'] = oheight, owidth
            # output format only accepts integer positions, round article sizes up
            # to even numbers
            width, height = roundeven(oheight), roundeven(owidth)
            node = find_node(root, width, height)
            if (node):
                node = split_node(node, width, height, article)
            else:
                rest.append(article)

    # gather all articles that were
    def find_articles(node):
        if not node['article']:
            return
        layer.append(node['article'])
        if node['right']:
            find_articles(node['right'])
        if node['down']:
            find_articles(node['down'])

    find_articles(root)

    return root, layer, rest

# generate a list of random articles of three different type
# each type is of the same size and color
# numbers of articles of each type linearly depend on their area
# articles are generated with more width that height
def generate_bin():
    abin = []
    for i in 1,2,3:
        w, h = random.randint(20,150), random.randint(20,150)
        if h > w:
            w, h = h, w
        color = random.randint(0,255), random.randint(0,255), random.randint(0,255)
        for j in range(200000/(w*h)):
            abin.append({'x':0, 'y':0, 'width':w, 'height':h, 'color':color})
    return abin

# given a node tree with articles inside, spread them out over the full
# available area evenly
def spread_articles(root):
    # get only nodes on the left branch of the tree. This is all nodes below
    def get_down_nodes(node):
        if node is None or not node['article']:
            return []
        else:
            return [node] + get_down_nodes(node['down'])

    # move this node and its whole subtree down
    def move_tree_down(node, y):
        if not node['article']:
            return
        node['article']['y'] += y
        if node['right']:
            move_tree_down(node['right'], y)
        if node['down']:
            move_tree_down(node['down'], y)

    # for each child on the very left, spread vertically and adjust subtree y
    # position accordingly
    def spread_vertically(node):
        downnodes = get_down_nodes(node)
        # process innermost nodes before outer nodes
        for n in downnodes:
            if n['right']:
                spread_vertically(n['right'])
        if len(downnodes) == 0:
            return
        elif len(downnodes) == 1:
            # arrange them in the center of parent
            # treat the article height as even and round the gap size to the
            # next smallest even number
            gap = (node['height']-roundeven(downnodes[0]['article']['height']))/4*2
            move_tree_down(node, gap)
        else:
            # get the sum of all heights of the leftmodes articles as if they
            # had even heights
            sumdownnodes = sum([roundeven(n['article']['height']) for n in downnodes])
            # do some fancy math to figure out even gap sizes between
            # the nodes
            d, m = divmod((node['height']-sumdownnodes)/2, len(downnodes)-1)
            gaps = (m)*[(d+1)*2]+((len(downnodes)-1)-m)*[d*2]
            # iteratively move trees down by vgap except for first row
            for node, gap in zip(downnodes[1:], gaps):
                move_tree_down(node, gap)

    # for a given node, return a tuple consisting of a list of nodes that
    # make out the longest row in horizontal direction and a list of nodes that
    # start a shorter end
    def get_max_horiz_nodes(node):
        if node is None or not node['article']:
            return [], []
        elif node['down'] and node['down']['article']:
            # if the node has an article below, check out the rightbranch
            rightbranch, sr = get_max_horiz_nodes(node['right'])
            rightbranch = [node] + rightbranch
            # as well as the down branch
            downbranch, sd = get_max_horiz_nodes(node['down'])
            # get information about the last article in each branch
            ar = rightbranch[len(rightbranch)-1]['article']
            ad = downbranch[len(downbranch)-1]['article']
            # and return as the first tuple entry the branch that stretches the
            # longest while having as the second tuple entry the nodes that
            # were dismissed as starting shorter branches
            if ar['x']+ar['width'] > ad['x']+ad['width']:
                return rightbranch, sr+[downbranch[0]]
            else:
                return downbranch, sd+[rightbranch[0]]
        else:
            # if there is no article below, just recursively call itself on the
            # next node to the right
            rightbranch, short = get_max_horiz_nodes(node['right'])
            return [node] + rightbranch, short

    # move a node and the article inside to the right and reduce node width
    # recursively call for children
    def move_tree_right(node, x):
        if not node['article']:
            return
        node['article']['x'] += x
        node['x'] += x
        node['width'] -= x
        if node['right']:
            move_tree_right(node['right'], x)
        if node['down']:
            move_tree_right(node['down'], x)

    # for each child on the very right, spread horizontally and adjust subtree
    # x position accordingly
    def spread_horizontally(node):
        maxhoriznodes, short = get_max_horiz_nodes(node['right'])
        maxhoriznodes = [node] + maxhoriznodes
        if len(maxhoriznodes) == 0:
            return
        elif len(maxhoriznodes) == 1:
            # arrange them in the center of parent
            # treat article width as even and round the gap size to the next
            # smallest even number
            gap = (node['width']-roundeven(maxhoriznodes[0]['article']['width']))/4*2
            maxhoriznodes[0]['article']['x'] += gap
        else:
            # get the sum of all widths of the articles that make the longest
            # row of articles as if they had even widths
            summaxhoriznodes= sum([roundeven(n['article']['width']) for n in maxhoriznodes])
            # do some fancy math to figure out even gap sizes between the nodes
            d, m = divmod((node['width']-summaxhoriznodes)/2, len(maxhoriznodes)-1)
            gaps = (m)*[(d+1)*2]+((len(maxhoriznodes)-1)-m)*[d*2]
            # iteratively move trees right by hgap except for first node
            for node, gap in zip(maxhoriznodes[1:], gaps):
                move_tree_right(node, gap)
            # recursively call for all nodes starting a shorter subtree
            for node in short:
                spread_horizontally(node)

    # spread nodes vertically
    spread_vertically(root)

    # and horizontally
    for node in get_down_nodes(root):
        spread_horizontally(node)

# sanity checks
def sanity_check(layer):
    def intersects(a1, a2):
        return (a1['x']              < a2['x']+a2['width']
            and a1['x']+a1['width']  > a2['x']
            and a1['y']              < a2['y']+a2['height']
            and a1['y']+a1['height'] > a2['y'])
    odds = list()
    overhangs = list()
    inters = list()
    for article1 in layer:
        if (article1['x']%2 != 0
         or article1['y']%2 != 0):
            odds.append(article1)
        if (article1['x'] < 0
         or article1['y'] < 0
         or article1['x']+article1['width'] > pwidth
         or article1['y']+article1['height'] > pheight):
            overhangs.append(article1)
        for article2 in layer:
            if article1 == article2:
                continue
            if intersects(article1, article2):
                inters.append((article1, article2))
    for odd in odds:
        print "odd:", odd
    for overhang in overhangs:
        print "overhang:", overhang
    for inter in inters:
        print "intersect:", inter
    if len(odds) or len(overhangs) or len(inters):
        print layer
        exit(1)

# draw layer of articles
def draw_layer(filename, layer):
    scene = svg.Scene(filename, (pwidth, pheight))
    for a in layer:
        scene.add(svg.Rectangle((a['x'], a['y']),(a['width'], a['height']),a['color']))
    scene.write()

# return all articles in a node tree
def find_articles(node):
    if node is None or not node['article']:
        return []
    else:
        return [node['article']] + find_articles(node['right']) + find_articles(node['down'])

if __name__ == "__main__":
    abin = generate_bin()
    pwidth, pheight = 800, 600
    abin = sorted(abin, key=lambda article: article['width']*article['height'], reverse=True)
    root, layer, rest = arrange_in_layer(abin, pwidth, pheight)
    draw_layer('test1', layer)
    spread_articles(root)
    layer = find_articles(root)
    draw_layer('test2', layer)
    sanity_check(layer)
