package WWW::WebKit2::Locator;

use Carp qw(croak);
use JSON qw(decode_json encode_json);
use Moose;

has 'locator_string' => (
    is       => 'ro',
    isa      => 'Str',
    required => 1,
);

has 'locator_parent' => (
    is  => 'ro',
    isa => 'Maybe[WWW::WebKit2::Locator]',
);

has 'inspector' => (
    is       => 'ro',
    isa      => 'WWW::WebKit2',
    required => 1,
);

has 'resolved_locator' => (
    is       => 'ro',
    isa      => 'Str',
    default  => sub {
        my ($self) = @_;

        return $self->resolve_locator;
    }
);

=head2 is_visible_function

Taken from jQuery's codebase.
https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom

=cut

has 'is_visible_function' => (
    is       => 'ro',
    isa      => 'Str',
    default  =>  sub {
        return qq{
            function isVisible(e) {
                if (e == undefined) {
                    return 0;
                }

                if(getComputedStyle(e).visibility === 'hidden') {
                    return 0;
                }

                var visible = !!( e.offsetWidth || e.offsetHeight || e.getClientRects().length );
                return visible ? 1 : 0;
            };
        };
    },
);

=head2 get_text

=cut

sub get_text {
    my ($self) = @_;

    my $text = $self->property_search('textContent');
    $text =~ s/\A \s+ | \s+ \z//gxm;
    $text =~ s/\s+/ /gxms; # squeeze white space
    return $text;
}

=head2 get_inner_html

=cut

sub get_inner_html {
    my ($self) = @_;

    return $self->property_search('innerHTML');
}

=head2 set_inner_text

=cut

sub set_inner_text {
    my ($self, $value) = @_;

    return $self->property_search('innerText = "' . $value . '";');

}

=head2 set_inner_html

=cut

sub set_inner_html {
    my ($self, $value) = @_;

    return $self->property_search('innerHTML = "' . $value . '";');
}

=head2 get_tag_name

=cut

sub get_tag_name {
    my ($self) = @_;

    return $self->property_search('tagName');
}

=head2 get_id

=cut

sub get_id {
    my ($self) = @_;

    return $self->property_search('id');
}

=head2 get_node_name

=cut

sub get_node_name {
    my ($self) = @_;

    return $self->property_search('tagName');
}

=head2 get_attribute

=cut

sub get_attribute {
    my ($self, $attribute) = @_;

    my $value = $self->property_search("getAttribute('$attribute')");

    return $value;
}

=head2 set_attribute

=cut

sub set_attribute {
    my ($self, $attribute, $value) = @_;

    $value =~ s/'/\\'/gis;

    return $self->property_search("setAttribute('$attribute', '$value')");
}

=head2 remove_attribute

=cut

sub remove_attribute {
    my ($self, $attribute) = @_;

    return $self->property_search("removeAttribute('$attribute')");
}

=head2 get_property

=cut

sub get_property {
    my ($self, $property) = @_;

    return $self->get_attribute($property);
}

=head2 get_offset_width

=cut

sub get_offset_width {
    my ($self) = @_;

    return $self->property_search('offsetWidth');
}

=head2 get_offset_height

=cut

sub get_offset_height {
    my ($self) = @_;

    return $self->property_search('offsetHeight');
}

=head2 get_offset_top

=cut

sub get_offset_top {
    my ($self) = @_;

    return $self->property_search('offsetTop');
}

=head2 get_offset_left

=cut

sub get_offset_left {
    my ($self) = @_;

    return $self->property_search('offsetLeft');
}

=head2 get_checked

=cut

sub get_checked {
    my ($self) = @_;

    return $self->property_search("checked");
}

=head2 get_value

=cut

sub get_value {
    my ($self) = @_;

    return $self->property_search('value');
}

=head2 set_value

=cut

sub set_value {
    my ($self, $value) = @_;

    $value =~ s/"/\\"/g; # sanitize input
    return $self->property_search('value = "' . $value . '";');
}

=head2 get_length

=cut

sub get_length {
    my ($self) = @_;

    my $search = $self->prepare_elements_search('length');

    return $self->inspector->run_javascript($search);
}

=head2 scroll_into_view

=cut

sub scroll_into_view {
    my ($self) = @_;

    return $self->property_search('scrollIntoView()');
}

=head2 get_screen_position

=cut

sub get_screen_position {
    my ($self) = @_;

    my $search = $self->prepare_element .
        'var offset_parent = element;
        var root_element = document.body;
        var x = 0;
        var y = 0;
        while (offset_parent && offset_parent != root_element && offset_parent.nodeType) {
            x += offset_parent.offsetLeft || 0;
            y += offset_parent.offsetTop || 0;
            offset_parent = offset_parent.offsetParent;
        }
        var win_left = window.screenLeft ? window.screenLeft : window.screenX;
        var win_top = window.screenTop ? window.screenTop : window.screenY;
        var result = { "y": win_top + y, "x": win_left + x };
        return JSON.stringify(result)';

    my $result = decode_json $self->inspector->run_javascript($search);
    return ($result->{x}, $result->{y});
}

=head2 focus

=cut

sub focus {
    my ($self) = @_;

    return $self->property_search('focus()');
}

=head2 submit

=cut

sub submit {
    my ($self) = @_;

    $self->property_search('submit()');

    $self->inspector->wait_for_condition(sub {
        $self->inspector->load_status eq 'started'
    });

    $self->inspector->process_page_load;

    return 1;
}

=head2 fire_event

=cut

sub fire_event {
    my ($self, $event_type) = @_;

    my $fire_event = $self->prepare_element . '
        window.event_fired = "initialized";
        element.addEventListener("' . $event_type . '", function(e) {
           window.event_fired = "fired";
        }, { once: true });
        var event = new Event("' . $event_type . '", { "bubbles": true, "cancelable": true });
        element.dispatchEvent(event);
        ';

    my $result = $self->inspector->run_javascript($fire_event);
    $self->inspector->wait_for_condition(sub {
        my $event_fired = $self->inspector->run_javascript("return window.event_fired");
        # event_fired will be undef if the event triggered a page load
        return 1 if (not $event_fired or $event_fired eq "fired");
        return 0;
    });

    # regardless of what javascript will be executed because of fire_event,
    # at least make sure to wait until we have a page to work with.
    Gtk3::main_iteration_do(0)
        while (Gtk3::events_pending);

    return $result;
}

=head2 is_visible

=cut

sub is_visible {
    my ($self, $attribute) = @_;

    my $search = $self->prepare_element;

    my $is_visible_function = $self->is_visible_function;

    $search .= "
        $is_visible_function
        return isVisible(element)
    ";

    return $self->inspector->run_javascript($search);
}

=head2 resolve_locator

=cut

sub resolve_locator {
    my ($self) = @_;

    my $locator = $self->locator_string;

    if (my ($label) = $locator =~ /^label=(.*)/) {
        return $label eq ''
            ? qq{.//*[not(text())]} : qq{.//*[text()="$label"]};
    }
    elsif (my ($link) = $locator =~ /^link=(.*)/) {
        return $link eq ''
            ? qq{.//a[not(descendant-or-self::text())]}
            : qq{.//a[descendant-or-self::text()="$link"]};
    }
    elsif (my ($value) = $locator =~ /^value=(.*)/) {
        return qq{.//*[\@value="$value"]};
    }
    elsif (my ($index) = $locator =~ /^index=(.*)/) {
        return qq{.//option[position()="$index"]};
    }
    elsif (my ($id) = $locator =~ /^id=(.*)/) {
        return qq{.//*[\@id="$id"]};
    }
    elsif (my ($class) = $locator =~ /^class=(.*)/) {
        return qq{.//*[contains(\@class, "$class")]};
    }
    elsif (my ($name) = $locator =~ /^name=(.*)/) {
        return qq{.//*[\@name="$name"]};
    }
    elsif (my ($xpath) = $locator =~ /^(?: xpath=)?(.*)/xm) {
        return $xpath;
    }
    elsif (my ($xpath_fallback) = $locator =~ /^(\/\/.*)/xm) {
        return $xpath_fallback;
    }

    return;
}

my $get_elements_function = q{
    function getElementsByXPath(xpath, parent) {
        let results = [];
        let query = document.evaluate(xpath, parent || document,
            null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        for (let i = 0, length = query.snapshotLength; i < length; ++i) {
            results.push(query.snapshotItem(i));
        }
        return results;
    };
};

=head2 prepare_element

=cut

sub prepare_element {
    my ($self, $element_name) = @_;

    $element_name //= 'element';

    my $locator = $self->resolved_locator;

    my ($parent, $parent_param) = ('', '');

    if ($self->locator_parent) {

        $parent = $self->locator_parent->prepare_element('parent');
        $parent_param = ", parent";
    }

    my $search = "
        $get_elements_function
        $parent
        var $element_name = getElementsByXPath('$locator'$parent_param);
    ";

    my $count = $self->inspector->run_javascript($search . "return $element_name.length;");
    croak "xpath: $locator gave $count results" if $count != 1;

    $search .= "$element_name = $element_name" . "[0];";

    return $search;
}

=head2 prepare_element_search

=cut

sub prepare_element_search {
    my ($self, $function) = @_;

    my $search = $self->prepare_element;
    $search .= "return element.$function;";

    return $search;
}

=head2 prepare_elements

=cut

sub prepare_elements {
    my ($self) = @_;

    my $locator = $self->resolved_locator;

    my $search = "
        $get_elements_function
        var elements = getElementsByXPath('$locator');
    ";

    return $search;
}

=head2 prepare_elements_search

=cut

sub prepare_elements_search {
    my ($self, $function) = @_;

    my $search = $self->prepare_elements;
    $search .= "return elements.$function;";

    return $search;
}

=head2 property_search

=cut

sub property_search {
    my ($self, $property) = @_;

    my $search = $self->prepare_element_search($property);

    return $self->inspector->run_javascript($search);
}

__PACKAGE__->meta->make_immutable;

1;
