# 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 POSIX qw|ceil|;
use strict;

# At the time of the original version of this function, the existing
# Randr-modules on CPAN seem to work only barely, so instead we just parse the
# output of xrandr --listactivemonitors. This is an uglyness, that should go
# away some time in the future.
sub get_outputs {
    my @outputs = ();
    for my $line (qx(xrandr --listactivemonitors)) {
        next if $line =~ /Monitors: /;

        # output looks like:
        #Monitors: 2
        # 0: +*LVDS-1 1366/277x768/156+0+0  LVDS-1
        # 1: +HDMI-2 1920/518x1200/324+1366+0  HDMI-2
        my ($w, $h, $x, $y) = ($line =~ /(\d+)\/\d+x(\d+)\/\d+\+(\d+)\+(\d+)/);
        print "found monitor with dimensions and position w=$w h=$h x=$x y=$y\n";
        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) {
        print "Fall back to getting shellex-position from pointer\n";
        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};
        }
    }
}

# 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) = @_;

    # TODO: Remove compatibility code in future version
    if (defined $self->x_resource("%.edge") || defined $self->x_resource("%.pos")) {
        print
"WARNING: URxvt.shellex.* resources are deprecated and will be removed in the future. Use shellex.*\n";
    }

    if ($self->x_resource("edge") eq 'bottom' || $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' || $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
# TODO revisit communication protocol (from file to pipe?)
# TODO check if user defined their own SHELLEX_MAX_ROWS, which should be used like
#$ENV{SHELLEX_MAX_ROWS} = $sane_max_rows < $ENV{SHELLEX_MAX_ROWS} ? $sane_max_rows : $ENV{SHELLEX_MAX_ROWS} ;
#
# shellex should leave part of the screen uncovered (10 lines), this assumes
# that a screen will always be larger than 10 lines.
    my $sane_max_rows = int($self->{h} / $self->fheight) - 10;

    $ENV{SHELLEX_MAX_ROWS} = $sane_max_rows;
    my $filename = $ENV{SHELLEX_SIZE_FILE};
    open(my $fh, '>', $filename);
    print $fh "$ENV{SHELLEX_MAX_ROWS}\n";
    close $fh;
    print "wrote $sane_max_rows as max rows to $filename done\n";

    $self->{border} = $self->x_resource('internalBorder');

    # the compiled-in default if the resource is not set
    $self->{border} //= 2;

    $self->{border} += $self->x_resource('externalBorder');

    $self->{row_height} = $self->fheight + $self->x_resource('lineSpace');

    my $height = $self->{row_height} + 2 * $self->{border};
    my $y      = $self->{y};

    # Our initial position is different, if we have to be at the bottom
    $y -= $height if $self->{bottom};

    $self->XMoveResizeWindow($self->parent, $self->{x}, $y, $self->{w}, $height);

    print "loading config\n";
    $self->tt_write($self->locale_encode("unset LD_PRELOAD\n"));
    $self->tt_write($self->locale_encode(". /etc/shellexrc\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 > $ENV{SHELLEX_MAX_ROWS} ? $ENV{SHELLEX_MAX_ROWS} : $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);
    }
    print "Predicting term size: $n\n";
    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);
    $nrow = $nrow > $ENV{SHELLEX_MAX_ROWS} ? $ENV{SHELLEX_MAX_ROWS} : $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");
    ();
}

# 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);
    ();
}
