#!/usr/bin/env bash

# tile all the windows on the current desktop
# if $1 is given then filter windows title
# if $2 is given then use it as a right edge

# Copyright (C) 2008-2013 Bob Hepple
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# http://bhepple.freeshell.org

check_and_process_opts() {
    ARGS="
ARGP_DELETE=quiet
ARGP_VERSION=$VERSION
ARGP_PROG=$PROG
##############################################################   
#OPTIONS:
#name=default  sname arg       type range   description
##############################################################   
PAD=''         p     'padding' b    ''      Extra padding - up to 4 numbers left,right,top,bottom.
NO_RESET=''    r     ''        b    ''      don't reset placement to origin when first window fails to fit.
AVOID=''       a     ''        b    ''      avoid placing windows astride the centre - mainly for dual display.
NO_STRICT=''   s     ''        b    ''      don't place strictly - not all parts of every window need be onscreen.
HORIZONTAL=''  H     ''        b    ''      place horizontally (default is vertical placement).
SMALL_FIRST='' S     ''        b    ''      place smallest windows first (default is largest first).
HALF=''        2     ''        b    ''      set all windows to 1/2 screen (1x2).
THIRD=''       3     ''        b    ''      set all windows to 1/3 screen (3x1).
QUARTER=''     4     ''        b    ''      set all windows to 1/4 screen (2x2).
SIXTH=''       6     ''        b    ''      set all windows to 1/6 screen (3x2).
EIGHTH=''      8     ''        b    ''      set all windows to 1/8 screen (4x2).
NINTH=''       9     ''        b    ''      set all windows to 1/9 screen (3x3).
RATIO=''       ''    'NxM'     b    ''      set all windows to any fraction of the screen (N horizontally and M vertically).
ALL=''         A     ''        b    ''      Move all windows on all consoles.
NO_FLUXBOX=''  f     ''        b    ''      don't use fluxbox commands.
DRY_RUN=''     n     ''        b    ''      dry run.
DEBUG=''       ''    ''        b    ''      debug.
##############################################################   
ARGP_ARGS=[--] $ARGUMENTS
ARGP_SHORT=$SHORT_DESC
ARGP_USAGE=$USAGE"

    exec 4>&1
    eval $(echo "$ARGS" | argp.sh "$@" 3>&1 1>&4 || echo exit $? )
    exec 4>&-


    [[ "$HALF" ]] && X_RATIO=2 && Y_RATIO=1
    [[ "$THIRD" ]] && X_RATIO=3 && Y_RATIO=1
    [[ "$QUARTER" ]] && X_RATIO=2 && Y_RATIO=2
    [[ "$SIXTH" ]] && X_RATIO=3 && Y_RATIO=2
    [[ "$EIGHTH" ]] && X_RATIO=4 && Y_RATIO=2
    [[ "$NINTH" ]] && X_RATIO=3 && Y_RATIO=3
    [[ "$RATIO" ]] && { X_RATIO=${2%x*}; Y_RATIO=${2#*x}; }

    NEW_ARGS=( "$@" )
    return 0
}

initialise() {
    set -eu

    PROG=$(basename $0)
    VERSION="1.3"
    SHORT_DESC="Tile windows on current desktop. "
    TITLE_FILTER=""
    DESKTOP=""
    X_RATIO=
    Y_RATIO=
    DRY_RUN=
    WRIGGLE_ROOM=100 # minimum number of pixels to be visible (if not strict)
    ALL=
    STRICT='yes'
    RESET='yes'

    USAGE="Uses wmctrl(1) to (very simplistically) rearrange windows on \
the current desktop so that they are tiled as far as possible. No \
window will be moved off the desktop. If title filter (regex) is given then \
only those windows whose title matches (from 'wmctrl -l') will be tiled.
Once the screen is tiled, there is not much point in having the window
decorations - if the windows are resized, the window decorations
are removed. Assumes one or two screens of the same size."

    VERBOSE=""
    ARGUMENTS="[ title filter ]"

    NEW_ARGS=( )
    check_and_process_opts "$@"

    TITLE_FILTER="${NEW_ARGS[@]:-}"

    [[ "$NO_STRICT" ]] && STRICT=
    [[ "$NO_RESET" ]] && RESET=

    type wmctrl &> /dev/null || {
        echo "$PROG: wmctrl is not installed" >&2
        exit 1
    }

    [[ "$VERBOSE" ]] && VERBOSE=-v
    [[ "$DEBUG" ]] && VERBOSE=-d

    source xwin-utils
    [[ "$NO_FLUXBOX" ]] && XWIN_USE_FLUXBOX="false" && NO_FLUXBOX=-f

    set -- `xwin-current-desktop`
    CURRENT_DESKTOP=$1
    SIZE=$2
    SCREEN_X=${SIZE%x*}
    SCREEN_Y=${SIZE#*x}
    SIZE=$3
    DESKTOP_X=${SIZE%x*}
    DESKTOP_Y=${SIZE#*x}

    if [[ "$AVOID" ]]; then
        AVOID=""
        for CON in $( xwin-list-consoles ); do
            set -- $( xwin-get-con-geom $CON )
            X=$1 WIDTH=$3
            (( X == 0 )) && continue
            AVOID+="$WIDTH "
        done
        [[ "$VERBOSE" ]] && echo "avoiding AVOID"
    fi

    if [[ "$STRICT" ]]; then
        LIMIT_X=$(( DESKTOP_X ))
        LIMIT_Y=$(( DESKTOP_Y ))
    else
        LIMIT_X=$(( DESKTOP_X + WRIGGLE_ROOM ))
        LIMIT_Y=$(( DESKTOP_Y + WRIGGLE_ROOM ))
    fi
    [[ "$VERBOSE" ]] && echo "Limits are $LIMIT_X, $LIMIT_Y"

    ORDER=r
    [[ "$SMALL_FIRST" ]] && ORDER=""
    return 0
}

main() {
    LIMIT_TO_CONS=
    CONS_OFFSET_X=0
    CONS_OFFSET_Y=0
    CONS_LIST=( )
    CONS_LIST=( $( xwin-list-consoles ) )
	# find the console of the window with focus 
    FOCUS_WINDOW_ID=$( xwin-wid )
    if [[ -z "$ALL" ]] && (( ${#CONS_LIST[@]} > 1 )); then
        set -- $(xwin-geometry $FOCUS_WINDOW_ID); X=$1 Y=$2 WIDTH=$3 HEIGHT=$4
        LIMIT_TO_CONS=$( xwin-get-con $(( X + WIDTH / 2 )) $(( Y + HEIGHT / 2)) )
        set -- $( xwin-get-con-geom $LIMIT_TO_CONS ); CONS_OFFSET_X=$3 CONS_OFFSET_Y=$4
        [[ "$VERBOSE" ]] && echo "Limiting operations to $LIMIT_TO_CONS"
    fi
    NEW_X=$CONS_OFFSET_X
    NEW_Y=$CONS_OFFSET_Y
    MAX_X=$NEW_X
    MAX_Y=$NEW_Y

    [[ "$X_RATIO" ]] && {
        # resize all the windows:
        xwin-list-windows | 
        while read ID DESKTOP X Y WIDTH HEIGHT CLASS CLIENT TITLE; do
            [[ $DESKTOP == $CURRENT_DESKTOP ]] || continue
            [[ "$DEBUG" ]] && echo "$ID $DESKTOP $X $Y $WIDTH $HEIGHT $CLASS $CLIENT $TITLE" >&2
            if [[ "$LIMIT_TO_CONS" ]]; then
                WIN_CONS=$(xwin-get-con $(( X + WIDTH / 2 )) $(( Y + HEIGHT / 2)) )
                [[ $WIN_CONS == $LIMIT_TO_CONS ]] || continue
            fi
            [[ "$TITLE_FILTER" ]] && {
                [[ "$TITLE" =~ $TITLE_FILTER ]] || continue
            }
            CMD="half-screen $VERBOSE $NO_FLUXBOX --any ${X_RATIO}x${Y_RATIO} $ID"
            if [[ "$DRY_RUN" ]]; then
                echo "dry-run: $CMD"
            else
			    if xwin-using-fluxbox; then
                    xwin-raise-window $ID
                    $BH_ECHO_EXEC fluxbox-remote "SetDecor BORDER" || :
                    # else
                    #    toggle-decor
                fi
                $BH_ECHO_EXEC $CMD
            fi
        done || :
    }

    # gather, filter and sort window data. wmctrl format is 
    # id desktop x y width height client title
    xwin-list-windows | 
    while read ID DESKTOP X Y WIDTH HEIGHT CLASS CLIENT TITLE; do
        [[ $DESKTOP == $CURRENT_DESKTOP ]] || continue
        [[ "$DEBUG" ]] && echo "$ID $DESKTOP $X $Y $WIDTH $HEIGHT $CLASS $CLIENT $TITLE" >&2
        if [[ "$LIMIT_TO_CONS" ]]; then
            WIN_CONS=$(xwin-get-con $(( X + WIDTH / 2 )) $(( Y + HEIGHT / 2)) )
            [[ $WIN_CONS == $LIMIT_TO_CONS ]] || continue
        fi
        [[ "$TITLE_FILTER" ]] && {
            [[ "$TITLE" =~ $TITLE_FILTER ]] || continue
        }
        SIZE=$(( WIDTH * HEIGHT ))
        echo "$SIZE $ID $X $Y $WIDTH $HEIGHT $CLASS $TITLE"
    done | 
    # sort window data by size of window, class (contains no spaces),
    # current X and then by title (which may contain spaces, hence is
    # the last item and -k8 means to the end of line):
    sort -k1,1${ORDER}n -k7,7 -k3,3n -k8 |
    # do the moving:
    while read SIZE ID OLD_X OLD_Y WIDTH HEIGHT CLASS TITLE; do
        [[ "$VERBOSE" ]] && {
            echo "$SIZE $ID $OLD_X $OLD_Y $WIDTH $HEIGHT $CLASS $TITLE" 
        } >&2

        false && { # why did we need this? non-fluxbox perhaps?
            set -- $(xwin-padding $ID); L_PAD=$1 R_PAD=$2 T_PAD=$3 B_PAD=$4
            OLD_X=$(( OLD_X - L_PAD ))
            OLD_Y=$(( OLD_Y - T_PAD ))
            WIDTH=$(( WIDTH + L_PAD + R_PAD ))
            HEIGHT=$(( HEIGHT + T_PAD + B_PAD ))
        }
        RIGHT_EDGE=$((NEW_X + WIDTH))
        BOTTOM_EDGE=$((NEW_Y + HEIGHT))
        [[ "$VERBOSE" ]] && echo "$NEW_X $NEW_Y $RIGHT_EDGE $BOTTOM_EDGE"

        if [[ "$HORIZONTAL" ]]; then
            if (( $RIGHT_EDGE > $LIMIT_X )); then
                [[ "$VERBOSE" ]] && echo "Right edge ($RIGHT_EDGE) > limit ($LIMIT_X)"
                NEW_X=$CONS_OFFSET_X
                RIGHT_EDGE=$((NEW_X + WIDTH))
                NEW_Y=$MAX_Y
                BOTTOM_EDGE=$((NEW_Y + HEIGHT))
                [[ "$VERBOSE" ]] && echo "$NEW_X $NEW_Y $RIGHT_EDGE $BOTTOM_EDGE"
            fi
        else
            if (( $BOTTOM_EDGE > $LIMIT_Y )); then
                [[ "$VERBOSE" ]] && echo "Bottom edge ($BOTTOM_EDGE) > limit ($LIMIT_Y)"
                NEW_Y=$CONS_OFFSET_Y
                BOTTOM_EDGE=$((NEW_Y + HEIGHT))
                NEW_X=$MAX_X
                RIGHT_EDGE=$((NEW_X + WIDTH))
                [[ "$VERBOSE" ]] && echo "$NEW_X $NEW_Y $RIGHT_EDGE $BOTTOM_EDGE"
            fi
        fi

        for A in $AVOID; do
            if (( "$NEW_X" < "$A" && "$RIGHT_EDGE" > "$A" )); then
                [[ "$VERBOSE" ]] && echo "Avoiding $A"
                NEW_X="$A"
                RIGHT_EDGE=$((NEW_X + WIDTH))
                [[ "$VERBOSE" ]] && echo "$NEW_X $NEW_Y $RIGHT_EDGE $BOTTOM_EDGE"
                break
            fi
        done

        OFF_SCREEN=""
        if (( $NEW_X >= $DESKTOP_X || $NEW_Y >= $DESKTOP_Y )); then
            [[ "$VERBOSE" ]] && echo "Top left ($NEW_X) is off-screen"
            OFF_SCREEN="yes"
        fi
        if (( $RIGHT_EDGE > $LIMIT_X || $BOTTOM_EDGE > $LIMIT_Y )); then
            [[ "$VERBOSE" ]] && echo "Bottom right ($RIGHT_EDGE) is off-screen"
            OFF_SCREEN="yes"
        fi

        if [[ "$OFF_SCREEN" ]]; then
            if [[ "$RESET" ]]; then
                [[ "$VERBOSE" ]] && echo "Reset - starting again at origin"
                NEW_X=$CONS_OFFSET_X
                NEW_Y=$CONS_OFFSET_Y
                MAX_X=$NEW_X
                MAX_Y=$NEW_Y
                [[ "$VERBOSE" ]] && echo "$NEW_X $NEW_Y $RIGHT_EDGE $BOTTOM_EDGE"
            else
                echo "Can't move \"$TITLE\" without the --reset option"
                continue # can't move this window - maybe a smaller one
            fi
        fi

        if (( NEW_X != OLD_X || NEW_Y != OLD_Y )); then
            CMD="xwin-move-resize $ID $NEW_X $NEW_Y -1 -1"
            if [[ "$DRY_RUN" ]]; then
                echo "dry-run: move \"$TITLE\" to ($NEW_X,$NEW_Y)"
                echo "$CMD"
            else
                $BH_ECHO_EXEC $CMD
            fi
        else
            [[ "$VERBOSE" ]] && echo "No need to move"
        fi

        BOTTOM_EDGE=$((NEW_Y + HEIGHT))
        RIGHT_EDGE=$((NEW_X + WIDTH))

        if [[ "$HORIZONTAL" ]]; then
            NEW_X=$RIGHT_EDGE
            (( $BOTTOM_EDGE > $MAX_Y )) && MAX_Y=$BOTTOM_EDGE
        else
            NEW_Y=$BOTTOM_EDGE
            (( $RIGHT_EDGE > $MAX_X )) && MAX_X=$RIGHT_EDGE
        fi
    done || :

    xwin-raise-window $FOCUS_WINDOW_ID
    return 0
}

initialise "$@"
main
exit 0
