#!/usr/bin/env perl6
use v6.d;

class Model {
    has Lock::Async $!lock .= new;
    has BagHash $!words .= new;

    method add_words(@words --> Promise) {
        return $!lock.lock.then: {
            LEAVE $!lock.unlock;
            ++$!words{$_} for @words;
        }
    }

    method get_most_common_pairs(Int() $how_many=10 --> Promise) {
        return $!lock.lock.then: {
            LEAVE $!lock.unlock;
            reverse $!words.sort(*.value).head($how_many);
        }
    }
}

class View {
    method words_from_input(Str() $input --> Seq) {
        return $input.split(/\s+/,:skip-empty);
    }

    method table_from_ranked_pairs(@ranked_pairs) {
        return @ranked_pairs.map({ sprintf '%-15s %3d',$^p.key,$^p.value }).join("\n");
    }
}

class HTTPController {
    use Cro::HTTP::Router;

    has $.model is required;
    has $.view is required;

    sub respond($body) {
        response.status = 200;
        response.set-body("{$body}\n");
    }
 
    method routes() {
        return route {
            get -> *@, :$n! is query {
                self.get_words($n);
            }
            get -> *@ {
                self.get_words();
            }
            post -> *@ {
                request-body-text -> $body {
                    self.post_words($body);
                }
            }
        }
    }

    method get_words($n=10) {
        my @most_common_pairs = await $!model.get_most_common_pairs($n);
        my $table = $!view.table_from_ranked_pairs(@most_common_pairs);
        respond($table);
    }

    method post_words($body) {
        my @words = $!view.words_from_input($body);
        await $!model.add_words(@words);
        respond('ok');
    }
}

class LineController {
    has $.model is required;
    has $.view is required;

    method on_command(:$stream,:$command,:$args_string) {
        if $command eq 'get' {
            self.get_words($stream,$args_string);
        }
        elsif $command eq 'put' {
            self.put_words($stream,$args_string);
        }
        else {
            reply($stream,"bad command, only 'get' and 'put'");
        }
    }

    sub reply($stream,$text) {
        await $stream.print("$text\n");
    }

    method get_words($stream,$args_string) {
        my $how_many = $args_string || 10;

        my @most_common_pairs = await $!model.get_most_common_pairs($how_many);
        my $table = $!view.table_from_ranked_pairs(@most_common_pairs);

        reply($stream,$table);
    }

    method put_words($stream,$args_string) {
        my @words = $!view.words_from_input($args_string);
        await $!model.add_words(@words);

        reply($stream,'ok');
    }
}

class TextServer {

    has $.controller is required;
    has $.host is required;
    has $.port is required;

    has $!stop = Promise.new;

    method start() {
        start react {
            whenever IO::Socket::Async.listen($!host,$!port) -> $conn {
                whenever $conn.Supply.lines -> $line {
                    my ($command,$args_string) = $line.split(/\s+/,2);
                    $.controller.on_command(
                        stream => $conn,
                        :$command, :$args_string
                    );
                }
            }
            whenever $!stop -> {
                last;
            }
        };
    }

    method stop() { $!stop.keep }
}

use Cro::HTTP::Server;

my Model $model .= new;
my View $view .= new;

my HTTPController $httpcontroller .= new(:$model,:$view);
my $httpserver = Cro::HTTP::Server.new(
    host => 'localhost',
    port => 8080,
    application => $httpcontroller.routes,
);
$httpserver.start;

my LineController $linecontroller .= new(:$model,:$view);
my TextServer $textserver .=new(
    host => 'localhost',
    port => 2020,
    controller => $linecontroller,
);
$textserver.start;

react whenever signal(SIGINT) {
    $httpserver.stop;
    $textserver.stop;
    exit;
}
