# TODO: serialization trhu Queue prolly

#!/usr/bin/perl

use strict;
use warnings;
use lib "/home/dabreegster/lab/hexedui";
use include;

if (@ARGV and shift() eq "--stfu") {
  open STDERR, ">>/var/log/bewarez" or die "can't open stderr log: $!\n";
  print STDERR "-" x 80, "\n";
}

#sub POE::Kernel::ASSERT_DEFAULT () { 1 }
use POE ("Component::Server::TCP");
use PoCoDownload;
use HexedUI;
use Queue;
use Storable;

# Set up the queue.
my $dlqueue = new Queue;

my $entry = forge_template(
  ["<Name>", "(<Percent>%)", "<SoFar> MB of <Total> MB", "[<Speed> K/s]", "eta {<ETA>}"],
  Name => {
    Format => "-",
  },
  Percent => {
    Format => "100.0",
    Filter => sub {
      my $obj = shift;
      $obj->{Total} // return 0;
      sprintf("%.1f", $obj->{SoFar} / $obj->{Total} * 100)
    }
  },
  SoFar => {
    Format => "1024.42",
    Filter => sub {
      my $obj = shift;
      $obj->{SoFar} // return 0;
      sprintf("%.2f", $obj->{SoFar} / 1024 ** 2)
    }
  },
  Total => {
    Format => "1024.42",
    Filter => sub {
      my $obj = shift;
      $obj->{Total} // return 0;
      sprintf("%.2f", $obj->{Total} / 1024 ** 2)
    }
  },
  Speed => {
    Format => "100.0",
    Filter => sub {
      my $obj = shift;
      $obj->{Speed} // return 0;
      sprintf("%.1f", $obj->{Speed})
    }
  },
  ETA => {
    Format => "xx::yy::zz"
  }
);

# Set up the interface.
my $ui = cast HexedUI BeWarez => {
  Title => {
    Type   => "Fixed",
    At     => [0, 0],
    Size   => [3, "100%"],
    Border => 1
  },
  Main => {
    Type   => "Menu",
    At	   => [3, 0],
    Size   => ["100% - 3", "100%"],
    Border => 1,
    Pad    => 1,
    Queue  => $dlqueue,
    KeyHandler => sub {
      my (undef, $key) = @_;
      if ($key eq "p") {
        POE::Kernel->post("leecher", "toggle");
      } elsif ($key eq "<KEY_DC>") {
        POE::Kernel->post("leecher", "delete_dl");
      }
    },
    ShiftKeys => 1,
    DeleteKey => 0    # We want to do preprocessing on it ourselves
  },
  Shutdown => sub {
    POE::Kernel->call("leecher", "pause");
    POE::Kernel->post("leech", "shutdown");
    POE::Kernel->post("server", "shutdown");
    delete @{$dlqueue}{qw(Heap Hooks)};
    store($dlqueue, ".dlqueue");
  }
};

# Set up the session to handle commands.
POE::Component::Server::TCP->new(
  Port	      => 31008,
  Alias	      => "server",
  ClientInput => sub {
    my ($cmd, $arg) = split(" ", $_[ARG0], 2);
    if ($cmd eq "add") {
      POE::Kernel->post("leecher", "add_dload", $arg);
    } else {
      croak "I don't know the command $cmd!";
    }
  }
);

# Set up the session to talk HTTP.
POE::Component::Client::UserAgent->new(alias => "leech");

# Set up the session to handle downloading.
POE::Session->create(
  inline_states => {
    _start => sub {
      $_[KERNEL]->alias_set("leecher");
      $_[HEAP]{Alive} = 0;
      $ui->{BeWarez}{Title}->title(0, "Be Warez... I pwn your warez.");
      $ui->{BeWarez}{Title}->draw;

      $_[HEAP]{Queue} = $dlqueue;
      $dlqueue->hook(sub {
        my $queue = shift;
        my $cmd = shift;

        if ($cmd eq "shift") {
          my ($oldid, $newid) = @_;
          # We only need to do something if the first queue spot has been tampered with.
          return unless $oldid == 0 or $newid == 0;
          my $heap = $queue->{Heap}{Leecher};
          # And preserve our status (if we're active or not).
          my $status = $heap->{Alive};
          # A progress event might beat our pause event. Shouldn't because we call() pause
          # ASAP but just in case.
          $heap->{Alive} = 0;
          # Pause the old file if it was active.
          my $pauseme = $oldid || $newid;   # Pause the old '0'.
          POE::Kernel->call("leecher", "pause", $pauseme) if $status;
          # And start up the new one unless we were already paused.
          POE::Kernel->post("leecher", "resume") if $status;
        }

        # Save the new state of the queue in case we exit abnormally.
        if ($cmd eq "add" or $cmd eq "del" or $cmd eq "shift") {
          my $heap = delete $dlqueue->{Heap};
          my $hooks = delete $dlqueue->{Hooks};
          my $fh = delete $dlqueue->{Queue}[0]{File} if $dlqueue->all;
          # SAVE THE QUEUE! AND THE QUEEN!
          #store($dlqueue, ".dlqueue");
          ($dlqueue->{Hooks}, $dlqueue->{Heap}) = ($hooks, $heap);
          $dlqueue->{Queue}[0]{File} = $fh if $fh;
        }
      }, Leecher => $_[HEAP]);
      if ($startup) {
        $dlqueue->set($_, MenuFilter => $entry) foreach 0 .. $#{ $dlqueue->{Queue} };
        $ui->{BeWarez}{Main}->draw;
        POE::Kernel->post("leecher", "resume");
      }
    },
    add_dload => sub {
      my $url = $_[ARG0];
      return unless $url;
      my $queue = $_[HEAP]{Queue};
      my $fname;
      if ($url =~ m/youtube\.com/) {
        ($url, $fname) = clive_scrape($url);
      } else {
	      $fname = basename($url);
        # Ew, nasty URL encoding. Spaces aren't that much better, but eh...
        $fname =~ s/%20/ /g;
      }
      my $dl = {
        Name       => $fname,
        URL        => $url,
        MenuFilter => $entry
      };

      # Same file's already in the queue.
      debug("already in queue"), return if $queue->get($dl->{Name});
      $queue->add($dl->{Name}, %$dl);

      # Do we start it now or not?
      if ($queue->all == 1) {
        #$queue->set($dl->{Name}, Menu => "Starting download: $dl->{Name}...");
        $_[KERNEL]->yield("resume");
      } else {
        #$queue->set($dl->{Name}, Menu => "Queued download: $dl->{Name}...");
      }
    },
    resume => sub {
      $_[HEAP]{Alive} = 1;
      return unless my $dl = $_[HEAP]{Queue}->get(0);

      $dl->{SoFar} = -s $dl->{Name} // 0;
      open $dl->{File}, ">>$dl->{Name}" or croak "Can't append to $dl->{Name}: $!";
      $dl->{Request} = HTTP::Request->new("GET", $dl->{URL}, ["Range" => "bytes=$dl->{SoFar}-"]);
      $dl->{LastTime} = time;
      $dl->{LastSize} = $dl->{SoFar};
      $dl->{Speed} = 0;
      $dl->{ETA} = "Never";

      $_[KERNEL]->post(leech => "request",
        request   => $dl->{Request},
        response  => $_[SESSION]->postback("response"),
        callback  => \&progress,
        #chunksize => 1024
      );
    },
    pause => sub {
      my $id = $_[ARG0] // 0;
      return unless my $dl = $_[HEAP]{Queue}->get($id);
      $_[KERNEL]->post("leech", "cancel");
      $_[HEAP]->{Alive} = 0;
    
      close $dl->{File} if $dl->{File};
      delete @{$dl}{qw(SoFar File Request Total LastTime LastSize Speed)}
    },
    toggle => sub {
      $_[KERNEL]->yield( $_[HEAP]->{Alive} ? "pause" : "resume" );
    },
    response => sub {
      # We get this when a download is finished.
      my $response = $_[ARG1][1];
      my $http_code = $response->code;
      if ($response->is_success or $http_code == 416) {
        # 416 means we've already got the whole file.
        my $queue = $_[HEAP]{Queue};
        close $queue->get(0)->{File};
        $queue->del(0);
        $_[HEAP]{Alive} = 0;
        # Go to the next download... if it's there.
        $_[KERNEL]->yield("resume") if $queue->all;
      } elsif ($response->is_error) {
        debug "SCREAM! error $http_code"
      } elsif ($response->is_redirect) {
        debug "redirecting...";
        # Just update the request object so progress() doesn't freak out
        $_[HEAP]{Queue}->get(0)->{Request} = $response->{_request};
      } else {
        debug "Well wtf IS the response, then?!";
      }
    },
    delete_dl => sub {
      my $queue = $_[HEAP]{Queue};
      my $dl = $queue->get($queue->active);
      return unless my $opt = $ui->choose("Do you really want to delete $dl->{Name}?" =>
        "Just kidding.",
        "Cancel the download.",
        "Cancel the download and delete the file."
      );
      # Irregardless, remove the download from the queue.
      my $alive = $_[HEAP]{Alive};
      POE::Kernel->call("leecher", "pause") if $queue->active == 0;
      $queue->del($queue->active);
      POE::Kernel->call("leecher", "resume") if $alive;
      unlink($dl->{Name}) if $opt == 2; # Delete it, whatever...
    }
  }
);

$poe_kernel->run;
exit;

# Handle a chunk of data.

sub progress {
  my ($data, $response, undef) = @_;
  my $heap = $dlqueue->{Heap}{Leecher};
  debug("got a chunk, not not alive"), return unless $heap->{Alive};
  my $dl = $dlqueue->get(0);

  # Meaningful data from the response object.
  my $http_code = $response->code;
  my $http_req = $response->request;
  my $http_total = $response->header("content-length");

  # When we shift the queue, a cancel request is sent to the server. But it might not
  # process in time. So we want to check to make sure our current download matches the
  # response we got. But some redirects screw up and we have to match previous headers.
  debug("response got and request expected mismatch, old request"), return if "$dl->{Request}" ne "$http_req" and "$response->{_previous}{_request}" ne "$dl->{Request}";

  # The below block executes when we first start a download.
  unless ($dl->{Total}) {
    # Errors and stuff are handled by Poco::Client::UserAgent, so all we have to deal with
    # is good ol' data. :P
    debug "scary http code $http_code" unless $http_code == 206 or $http_code == 200;
    if ($http_total) {
      $dl->{Total} = $dl->{SoFar} + $http_total;
    } else {
      debug "no content-length header. stupid server. bugginess expected.";
      $dl->{Total} = -1;
    }
  }

  debug "sofar=total but got data" if $dl->{SoFar} == $dl->{Total} and $dl->{Total};
  debug "empty chunk received" unless $data;

  my $fh = $dl->{File};
  print $fh $data;
  $dl->{SoFar} += length($data);

  # Calculate speed
  my ($time, $size) = (time, $dl->{SoFar});
  if ($time - $dl->{LastTime} > 1) {
    $dl->{Speed} = ($size - $dl->{LastSize}) / ($time - $dl->{LastTime}) / 1024;
    ($dl->{LastTime}, $dl->{LastSize}) = ($time, $size);

    # Recalculate ETA
    my $secs = ($dl->{Total} - $dl->{SoFar}) / 1024 / $dl->{Speed};
    my $ok = sprintf("%02d:%02d:%02d", int($secs / 3600), ($secs % 3600) / 60, ($secs %3600)
    % 60);
    $dlqueue->set($dl->{ID}, ETA => $ok);
  }
  $dlqueue->set($dl->{ID}, dummy => "just to update menufilter");

  return 1;
}

# Gimme URL from youtube or similar
sub clive_scrape {
  my $rawurl = shift;
  chomp(my $out = (`clive --emit $rawurl`)[-1]);
  my (undef, $url, $fname, undef) = split(",", $out);
  $url =~ s/"//g;
  $fname =~ s/"//g;
  return ($url, $fname);
}
