package Mojo::Transaction::WebSocket;
use Mojo::Base 'Mojo::Transaction';

use Compress::Raw::Zlib qw(Z_SYNC_FLUSH);
use List::Util qw(first);
use Mojo::JSON qw(encode_json j);
use Mojo::Util qw(decode encode trim);
use Mojo::WebSocket qw(WS_BINARY WS_CLOSE WS_CONTINUATION WS_PING WS_PONG WS_TEXT);

has [qw(compressed established handshake masked)];
has max_websocket_size => sub { $ENV{MOJO_MAX_WEBSOCKET_SIZE} || 262144 };

sub build_message {
  my ($self, $frame) = @_;

  # Text
  $frame = {text => encode('UTF-8', $frame)} if ref $frame ne 'HASH';

  # JSON
  $frame->{text} = encode_json($frame->{json}) if exists $frame->{json};

  # Raw text or binary
  if   (exists $frame->{text}) { $frame = [1, 0, 0, 0, WS_TEXT,   $frame->{text}] }
  else                         { $frame = [1, 0, 0, 0, WS_BINARY, $frame->{binary}] }

  # "permessage-deflate" extension
  return $frame unless $self->compressed;
  my $deflate = $self->{deflate}
    ||= Compress::Raw::Zlib::Deflate->new(AppendOutput => 1, MemLevel => 8, WindowBits => -15);
  $deflate->deflate($frame->[5], my $out);
  $deflate->flush($out, Z_SYNC_FLUSH);
  @$frame[1, 5] = (1, substr($out, 0, length($out) - 4));

  return $frame;
}

sub client_read  { shift->server_read(@_) }
sub client_write { shift->server_write(@_) }

sub closed {
  my $self = shift->completed;
  my @args = $self->{close} ? (@{$self->{close}}) : (1006);
  return $self->emit(finish => @args > 1 ? @args : (@args, undef));
}

sub connection { shift->handshake->connection }

sub finish {
  my $self = shift;

  my $close   = $self->{close} = [@_];
  my $payload = $close->[0] ? pack('n', $close->[0]) : '';
  $payload .= encode 'UTF-8', $close->[1] if defined $close->[1];
  $close->[0] //= 1005;
  $self->send([1, 0, 0, 0, WS_CLOSE, $payload])->{closing} = 1;

  return $self;
}

sub is_websocket {1}

sub kept_alive    { shift->handshake->kept_alive }
sub local_address { shift->handshake->local_address }
sub local_port    { shift->handshake->local_port }

sub parse_message {
  my ($self, $frame) = @_;

  $self->emit(frame => $frame);

  # Ping/Pong
  my $op = $frame->[4];
  return $self->send([1, 0, 0, 0, WS_PONG, $frame->[5]]) if $op == WS_PING;
  return undef                                           if $op == WS_PONG;

  # Close
  if ($op == WS_CLOSE) {
    return $self->finish unless length $frame->[5] >= 2;
    return $self->finish(unpack('n', substr($frame->[5], 0, 2, '')), decode('UTF-8', $frame->[5]));
  }

  # Append chunk and check message size
  @{$self}{qw(op pmc)} = ($op, $self->compressed && $frame->[1]) unless exists $self->{op};
  $self->{message} .= $frame->[5];
  my $max = $self->max_websocket_size;
  return $self->finish(1009) if length $self->{message} > $max;

  # No FIN bit (Continuation)
  return undef unless $frame->[0];

  # "permessage-deflate" extension (handshake and RSV1)
  my $msg = delete $self->{message};
  if ($self->compressed && $self->{pmc}) {
    my $inflate = $self->{inflate}
      ||= Compress::Raw::Zlib::Inflate->new(Bufsize => $max, LimitOutput => 1, WindowBits => -15);
    $inflate->inflate(($msg .= "\x00\x00\xff\xff"), my $out);
    return $self->finish(1009) if length $msg;
    $msg = $out;
  }

  $self->emit(json => j($msg)) if $self->has_subscribers('json');
  $op = delete $self->{op};
  $self->emit($op == WS_TEXT ? 'text' : 'binary' => $msg);
  $self->emit(message => $op == WS_TEXT ? decode 'UTF-8', $msg : $msg) if $self->has_subscribers('message');
}

sub protocol { shift->res->headers->sec_websocket_protocol }

sub remote_address { shift->handshake->remote_address }
sub remote_port    { shift->handshake->remote_port }
sub req            { shift->handshake->req }
sub res            { shift->handshake->res }

sub resume { $_[0]->handshake->resume and return $_[0] }

sub send {
  my ($self, $msg, $cb) = @_;
  $self->once(drain => $cb) if $cb;
  $msg = $self->build_message($msg) unless ref $msg eq 'ARRAY';
  $self->{write} .= Mojo::WebSocket::build_frame($self->masked, @$msg);
  return $self->emit('resume');
}

sub server_read {
  my ($self, $chunk) = @_;

  $self->{read} .= $chunk;
  my $max = $self->max_websocket_size;
  while (my $frame = Mojo::WebSocket::parse_frame(\$self->{read}, $max)) {
    $self->finish(1009) and last unless ref $frame;
    $self->parse_message($frame);
  }

  $self->emit('resume');
}

sub server_write {
  my $self = shift;
  $self->emit('drain') unless length($self->{write} //= '');
  $self->completed if !length $self->{write} && $self->{closing};
  return delete $self->{write};
}

sub with_compression {
  my $self = shift;

  # "permessage-deflate" extension
  $self->compressed(1) and $self->res->headers->sec_websocket_extensions('permessage-deflate')
    if ($self->req->headers->sec_websocket_extensions // '') =~ /permessage-deflate/;
}

sub with_protocols {
  my $self = shift;

  my %protos = map { trim($_) => 1 } split /,/, $self->req->headers->sec_websocket_protocol // '';
  return undef unless defined(my $proto = first { $protos{$_} } @_);

  $self->res->headers->sec_websocket_protocol($proto);
  return $proto;
}

1;

=encoding utf8

=head1 NAME

Mojo::Transaction::WebSocket - WebSocket transaction

=head1 SYNOPSIS

  use Mojo::Transaction::WebSocket;

  # Send and receive WebSocket messages
  my $ws = Mojo::Transaction::WebSocket->new;
  $ws->send('Hello World!');
  $ws->on(message => sub ($ws, $msg) { say "Message: $msg" });
  $ws->on(finish => sub ($ws, $code, $reason) { say "WebSocket closed with status $code." });

=head1 DESCRIPTION

L<Mojo::Transaction::WebSocket> is a container for WebSocket transactions, based on L<RFC
6455|https://tools.ietf.org/html/rfc6455> and L<RFC 7692|https://tools.ietf.org/html/rfc7692>.

=head1 EVENTS

L<Mojo::Transaction::WebSocket> inherits all events from L<Mojo::Transaction> and can emit the following new ones.

=head2 binary

  $ws->on(binary => sub ($ws, $bytes) {...});

Emitted when a complete WebSocket binary message has been received.

  $ws->on(binary => sub ($ws, $bytes) { say "Binary: $bytes" });

=head2 drain

  $ws->on(drain => sub ($ws) {...});

Emitted once all data has been sent.

  $ws->on(drain => sub ($ws) { $ws->send(time) });

=head2 finish

  $ws->on(finish => sub ($ws, $code, $reason) {...});

Emitted when the WebSocket connection has been closed.

=head2 frame

  $ws->on(frame => sub ($ws, $frame) {...});

Emitted when a WebSocket frame has been received.

  $ws->on(frame => sub ($ws, $frame) {
    say "FIN: $frame->[0]";
    say "RSV1: $frame->[1]";
    say "RSV2: $frame->[2]";
    say "RSV3: $frame->[3]";
    say "Opcode: $frame->[4]";
    say "Payload: $frame->[5]";
  });

=head2 json

  $ws->on(json => sub ($ws, $json) {...});

Emitted when a complete WebSocket message has been received, all text and binary messages will be automatically JSON
decoded. Note that this event only gets emitted when it has at least one subscriber.

  $ws->on(json => sub ($ws, $hash) { say "Message: $hash->{msg}" });

=head2 message

  $ws->on(message => sub ($ws, $msg) {...});

Emitted when a complete WebSocket message has been received, text messages will be automatically decoded. Note that
this event only gets emitted when it has at least one subscriber.

  $ws->on(message => sub ($ws, $msg) { say "Message: $msg" });

=head2 resume

  $tx->on(resume => sub ($tx) {...});

Emitted when transaction is resumed.

=head2 text

  $ws->on(text => sub ($ws, $bytes) {...});

Emitted when a complete WebSocket text message has been received.

  $ws->on(text => sub ($ws, $bytes) { say "Text: $bytes" });

=head1 ATTRIBUTES

L<Mojo::Transaction::WebSocket> inherits all attributes from L<Mojo::Transaction> and implements the following new
ones.

=head2 compressed

  my $bool = $ws->compressed;
  $ws      = $ws->compressed($bool);

Compress messages with C<permessage-deflate> extension.

=head2 established

  my $bool = $ws->established;
  $ws      = $ws->established($bool);

WebSocket connection established.

=head2 handshake

  my $handshake = $ws->handshake;
  $ws           = $ws->handshake(Mojo::Transaction::HTTP->new);

The original handshake transaction, usually a L<Mojo::Transaction::HTTP> object.

=head2 masked

  my $bool = $ws->masked;
  $ws      = $ws->masked($bool);

Mask outgoing frames with XOR cipher and a random 32-bit key.

=head2 max_websocket_size

  my $size = $ws->max_websocket_size;
  $ws      = $ws->max_websocket_size(1024);

Maximum WebSocket message size in bytes, defaults to the value of the C<MOJO_MAX_WEBSOCKET_SIZE> environment variable
or C<262144> (256KiB).

=head1 METHODS

L<Mojo::Transaction::WebSocket> inherits all methods from L<Mojo::Transaction> and implements the following new ones.

=head2 build_message

  my $frame = $ws->build_message({binary => $bytes});
  my $frame = $ws->build_message({text   => $bytes});
  my $frame = $ws->build_message({json   => {test => [1, 2, 3]}});
  my $frame = $ws->build_message($chars);

Build WebSocket message.

=head2 client_read

  $ws->client_read($data);

Read data client-side, used to implement user agents such as L<Mojo::UserAgent>.

=head2 client_write

  my $bytes = $ws->client_write;

Write data client-side, used to implement user agents such as L<Mojo::UserAgent>.

=head2 closed

  $tx = $tx->closed;

Same as L<Mojo::Transaction/"completed">, but also indicates that all transaction data has been sent.

=head2 connection

  my $id = $ws->connection;

Connection identifier.

=head2 finish

  $ws = $ws->finish;
  $ws = $ws->finish(1000);
  $ws = $ws->finish(1003 => 'Cannot accept data!');

Close WebSocket connection gracefully.

=head2 is_websocket

  my $bool = $ws->is_websocket;

True, this is a L<Mojo::Transaction::WebSocket> object.

=head2 kept_alive

  my $bool = $ws->kept_alive;

Connection has been kept alive.

=head2 local_address

  my $address = $ws->local_address;

Local interface address.

=head2 local_port

  my $port = $ws->local_port;

Local interface port.

=head2 parse_message

  $ws->parse_message([$fin, $rsv1, $rsv2, $rsv3, $op, $payload]);

Parse WebSocket message.

=head2 protocol

  my $proto = $ws->protocol;

Return negotiated subprotocol or C<undef>.

=head2 remote_address

  my $address = $ws->remote_address;

Remote interface address.

=head2 remote_port

  my $port = $ws->remote_port;

Remote interface port.

=head2 req

  my $req = $ws->req;

Handshake request, usually a L<Mojo::Message::Request> object.

=head2 res

  my $res = $ws->res;

Handshake response, usually a L<Mojo::Message::Response> object.

=head2 resume

  $ws = $ws->resume;

Resume L</"handshake"> transaction.

=head2 send

  $ws = $ws->send({binary => $bytes});
  $ws = $ws->send({text   => $bytes});
  $ws = $ws->send({json   => {test => [1, 2, 3]}});
  $ws = $ws->send([$fin, $rsv1, $rsv2, $rsv3, $op, $payload]);
  $ws = $ws->send($chars);
  $ws = $ws->send($chars => sub {...});

Send message or frame non-blocking via WebSocket, the optional drain callback will be executed once all data has been
written.

  # Send "Ping" frame
  use Mojo::WebSocket qw(WS_PING);
  $ws->send([1, 0, 0, 0, WS_PING, 'Hello World!']);

=head2 server_read

  $ws->server_read($data);

Read data server-side, used to implement web servers such as L<Mojo::Server::Daemon>.

=head2 server_write

  my $bytes = $ws->server_write;

Write data server-side, used to implement web servers such as L<Mojo::Server::Daemon>.

=head2 with_compression

  $ws->with_compression;

Negotiate C<permessage-deflate> extension for this WebSocket connection.

=head2 with_protocols

  my $proto = $ws->with_protocols('v2.proto', 'v1.proto');

Negotiate subprotocol for this WebSocket connection.

=head1 SEE ALSO

L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.

=cut