# vim:ft=perl
#line 3
# shellex - shell based launcher
#   This is the urxvt extension part of shellex.
# © 2013 Axel Wagner and contributors (see also: LICENSE)
use X11::Protocol;
use File::Temp qw|tempfile|;
use File::Basename qw|basename|;
use POSIX qw|ceil|;
use strict;

# The existing Randr-modules on CPAN seem to work only barely, so instead we
# just parse the output of xrandr -q. This is an uglyness, that should go away
# some time in the feature.
sub get_outputs {
    my @outputs = ();
    for my $line (qx(xrandr -q)) {
        next unless $line =~ /\sconnected/;
        my ($w, $h, $x, $y) = ($line =~ /(\d+)x(\d+)\+(\d+)\+(\d+)/);
        push @outputs, { w => $w, h => $h, x => $x, y => $y };
    }
    return @outputs;
}

# This takes a list of outputs and looks up the one, the mouse pointer
# currently is on.
sub geometry_from_ptr {
    my ($self) = @_;

    my @outputs = get_outputs();

    my $ptr = { $self->{X}->QueryPointer($self->DefaultRootWindow) };

    for my $output (@outputs) {
        if ($output->{x} <= $ptr->{root_x} && $ptr->{root_x} < $output->{x} + $output->{w}) {
            $self->{x} = $output->{x};
            if ($self->{bottom}) {
                # The real y-coordinate will change during execution, when the window grows
                $self->{y} = $output->{y} + $output->{h};
            } else {
                $self->{y} = $output->{y};
            }
            $self->{w} = $output->{w};
            $self->{h} = $output->{h};
        }
    }
}

# Helper, that take a list of numbers and return the max resp. min
sub max {
    my $max = shift;
    while (my $n = shift) {
        $max = $n > $max ? $n : $max;
    }
    return $max;
}
sub min {
    my $min = shift;
    while (my $n = shift) {
        $min = $n < $min ? $n : $min;
    }
    return $min;
}

# This takes a list of outputs and looks up the one, that contains most of the
# window having the input focus currently
sub geometry_from_focus {
    my ($self) = @_;

    my @outputs = get_outputs();

    # Look up the window that currently has the input focus
    my ($focus, $revert) = $self->{X}->GetInputFocus();

    # If the root-window is focused, we fall back to using the pointer-position
    if ($focus == $self->DefaultRootWindow) {
        return $self->geometry_from_ptr();
    }

    my $geom = { $self->{X}->GetGeometry($focus) };
    my ($fw, $fh) = ($geom->{width}, $geom->{height});

    print "Focus $focus (${fw}x${fh})\n";

    # The (x,y) coordinates we get are relative to the parent not the
    # root-window. So we just translate the coordinates of the upper-left
    # corner into the coordinate-system of the root-window
    my (undef, undef, $fx, $fy) = $self->{X}->TranslateCoordinates($focus, $self->DefaultRootWindow, 0, 0);

    # Returns the area (in pixel²) of the intersection of two rectangles.
    # To understand how it works, best draw a picture.
    my $intersection = sub {
        my ($x, $y, $w, $h) = @_;
        my $dx;
        if ($x < $fx) {
            $dx = $x + $w - $fx;
        } else {
            $dx = $fx + $fw - $x;
        }
        $dx = max(0, min($dx, $fw, $w));

        my $dy;
        if ($y < $fy) {
            $dy = $y + $h - $fy;
        } else {
            $dy = $fy + $fh - $y;
        }
        $dy = max(0, min($dy, $fh, $h));

        return $dx * $dy;
    };

    my $max_area = 0;
    for my $output (@outputs) {
        my $area = $intersection->($output->{x}, $output->{y}, $output->{w}, $output->{h});
        if ($area >= $max_area) {
            $max_area = $area;
            $self->{x} = $output->{x};
            if ($self->{bottom}) {
                # The real y-coordinate will change during execution, when the window grows
                $self->{y} = $output->{y} + $output->{h};
            } else {
                $self->{y} = $output->{y};
            }
            $self->{w} = $output->{w};
            $self->{h} = $output->{h};
        }
    }
}

sub slurp {
    open my $fh, '<', shift;
    local $/;
    <$fh>;
}

sub gen_conf {
    my ($cfg, $cfgname) = tempfile("/tmp/shellex-XXXXXXXX", UNLINK => 0);

    print $cfg "rm $cfgname\n";

    my %fileset;
    map { $fileset{basename($_)} = 1 } </etc/shellex/*>;
    map { $fileset{basename($_)} = 1 } <$ENV{HOME}/.shellex/*>;

    my @files = sort keys %fileset;

    for my $f (@files) {
        if (-e "$ENV{HOME}/.shellex/$f") {
            print $cfg slurp("$ENV{HOME}/.shellex/$f");
        } else {
            print $cfg slurp("/etc/shellex/$f");
        }
    }
    close($cfg);

    return $cfgname;
}

# This hook is run when the extension is first initialized, before any windows
# are created or mapped. There is not much work we can do here.
sub on_init {
    my ($self) = @_;

    $self->{X} = X11::Protocol->new($self->display_id);

    # Some reasonably sane values in case all our methods to determine a
    # geometry fails.
    $self->{x} = 0;
    $self->{y} = 0;
    $self->{w} = 1024;
    $self->{h} = 768;

    ();
}

# This hook is run after the window is created, but before it is mapped, so
# this is the place to set the geometry to what we want
sub on_start {
    my ($self) = @_;

    if ($self->x_resource("%.edge") eq 'bottom') {
        print "position should be at the bottom\n";
        $self->{bottom} = 1;
        $self->{y} = $self->{h};
    } else {
        print "position should be at the top\n";
    }

    if ($self->x_resource("%.pos") eq 'pointer') {
        print "Getting shellex-position from pointer\n";
        $self->geometry_from_ptr();
    } else {
        print "Getting shellex-position from focused window\n";
        $self->geometry_from_focus();
    }

    # This environment variable is used by the LD_PRELOAD ioctl-override to
    # determine the values to send to the shell
    $ENV{SHELLEX_MAX_HEIGHT} = int($self->{h} / $self->fheight);

    # Our initial position is different, if we have to be at the bottom
    if ($self->{bottom}) {
        $self->XMoveResizeWindow($self->parent, $self->{x}, $self->{y} - (2 + $self->fheight), $self->{w}, 2+$self->fheight);
    } else {
        $self->XMoveResizeWindow($self->parent, $self->{x}, $self->{y}, $self->{w}, 2+$self->fheight);
    }

    my $cfg = gen_conf();

    $self->tt_write($self->locale_encode(". $cfg\n"));
    ();
}

# This hook is run every time a line was changed. We do some resizing here,
# because this catches most cases where we would want to shrink our window.
sub on_line_update {
    my ($self, $row) = @_;
    print "line_update(row = $row)\n";

    # Determine the last row, that is not empty.
    # TODO: Does this work as intended, if there is an empty line in the
    # middle?
    my $nrow = 0;
    for my $i ($self->top_row .. $self->nrow-1) {
        if ($self->ROW_l($i) > 0) {
            $nrow++;
        }
    }
    $nrow = $nrow > 0 ? $nrow : 1;
    print "resizing to $nrow\n";

    # If the window is supposed to be at the bottom, we have to move the
    # window up a little bit
    my $y = $self->{y};
    if ($self->{bottom}) {
        $y -= 2+$nrow*$self->fheight;
    }
    $self->cmd_parse("\e[8;$nrow;t\e[3;$self->{x};${y}t");
    ();
}

# Predict the number of rows the terminal will have, after adding $string at
# the current position
sub predict_term_size {
    my ($self, $string) = @_;

    my ($row, $col) = $self->screen_cur();
    my $i = $self->top_row;
    my $n = 0;

    # We iterate over all lines and accumulate the number of rows. If the
    # curser is not at the current line, we can just add its number of rows to
    # the total, else we test, if it grows when adding the string and add an
    # according number to the total
    while ($i < $self->nrow) {
        my $line = $self->line($i);
        $i += $line->end - $line->beg + 1;
        unless ($line->beg <= $row && $row <= $line->end) {
            $n += $line->end - $line->beg + 1;
            next
        }

        my $len = ($row - $line->beg) * $self->ncol + $col;

        # Because there might be control-sequences in $string, affecting the
        # number of lines, we need to manually walk it
        for (my $j = 0; $j < length($string); $j++) {
            # Linebreaks mean the creating of a new line, finishing the old one
            if (substr($string, $j, 1) eq "\n") {
                $len = ($len == 0 ? 1 : $len);
                $n += ceil(($len * 1.0) / $self->ncol);
                $len = 0;
                next;
            }
            # Carriage-returns mean starting from the beginning. Though the new
            # len does not really have to be 0 (because the text is not
            # actually erased) it is a good enough estimate for now
            if (substr($string, $j, 1) eq "\r") {
                $len = 0;
                next;
            }
            # We just add one per other char. This actually might not work
            # correctly with wide-chars, but it is a good enough estimate for
            # now
            $len++;
        }
        $n += ceil(($len + 1.0) / $self->ncol);
    }
    return $n;
}

# This hook is run every time before there is text output. We resize here,
# immediately before new lines would be added, which would create scrolling
sub on_add_lines {
    my ($self, $string) = @_;
    my $str = $string;
    $str =~ s/\n/\\n/g;
    $str =~ s/\r/\\r/g;
    print "add_lines(string = \"$str\")\n";

    my $nrow = $self->predict_term_size($string);
    print "resizing to $nrow\n";

    # If the window is supposed to be at the bottom, we have to move the
    # window up a little bit
    my $y = $self->{y};
    if ($self->{bottom}) {
        $y -= 2+$nrow*$self->fheight;
    }
    $self->cmd_parse("\e[8;$nrow;t\e[3;$self->{x};${y}t");
    ();
}

# Just for debugging
sub on_size_change {
    my ($self, $nw, $nh) = @_;
    print "size_change($nw, $nh)\n";
    ();
}

sub on_view_change {
    my ($self, $offset) = @_;
    print "view_change(offset = $offset)\n";
    ();
}

sub on_scroll_back {
    my ($self, $lines, $saved) = @_;
    print "scroll_back(lines = $lines, saved = $saved)\n";
    ();
}

sub on_x_event {
    my ($self, $event) = @_;

    if ($event->{type} == urxvt::EnterNotify) {
        $self->{X}->SetInputFocus($self->parent, 2, $self->{data}{event}{time});
        $self->{X}->GetInputFocus();
    }

    ();
}

# This hook is run directly after the window was mapped (= displayed on
# screen). We grab the keyboard here.
sub on_map_notify {
    my ($self, $ev) = @_;

    $self->{X}->SetInputFocus($self->parent, 2, $self->{data}{event}{time});
    # We use GetInputFocus as a syncing-mechanism
    $self->{X}->GetInputFocus();

    $self->vt_emask_add(urxvt::EnterWindowMask);
    ();
}
