# Copyright © 2016 by Yaroslav Kolomiiets
package Plan9;
use strict;
use Symbol;
	use Data::Dumper;

sub _getreq($) {
	my ($srv) = @_;
	my ($f, $r);

	do {
		lock($srv->{rlock});
		$f = readfcall($srv->{ior}, $srv->{msize});
	};
	if(!defined $f){
		return undef;
	}
	$r = {
		srv => $srv,
		responded => 0,
		ifcall => $f,
		ofcall => {},
		type => $f->{type},
		tag => $f->{tag},
	};

	if($::chatty9p){
		print STDERR sprintf("<-%d- %s\n", fileno($srv->{ior}), fcallstr(%$f));
	}
	
	return $r;	
}

sub srv($) {
	my ($srv) = @_;
	my ($r, $fntab, $fn);

	if(!defined $srv->{msize}){
		$srv->{msize} = 8192;
	}

	$fntab = {
		&Tversion =>	\&sversion,
		&Tauth =>	\&sauth,
		&Tattach =>	\&sattach,
		&Tflush =>	\&sflush,
		&Twalk =>	\&swalk,
		&Topen =>	\&sopen,
		&Tcreate =>	\&screate,
		&Tread =>	\&sread,
		&Twrite =>	\&swrite,
		&Tclunk =>	\&sclunk,
		&Tremove =>	\&sremove,
		&Tstat =>	\&sstat,
		&Twstat => \&swstat,
	};

	while(defined($r = _getreq($srv))){
		$fn = $fntab->{$r->{type}};
		if(!defined $fn){
			respond($r, "unknown message");
		}else{
			&$fn($r);
		}
	}
}

sub respond($;$) {
	my ($r, $error) = @_;
	my ($srv, $fntab, $fn);

	$srv = $r->{srv};
	die unless defined $srv;

	die if $r->{responded};
	$r->{error} = $error;

	$fntab = {
		&Tversion =>	\&rversion,
		&Tauth =>	\&rauth,
		&Tattach =>	\&rattach,
		&Tflush =>	\&rflush,
		&Twalk =>	\&rwalk,
		&Topen =>	\&ropen,
		&Tcreate =>	\&rcreate,
		&Tread =>	\&rread,
		&Twrite =>	\&rwrite,
		&Tclunk =>	\&rclunk,
		&Tremove =>	\&rremove,
		&Tstat =>	\&rstat,
		&Twstat => \&rwstat,
	};
	$fn = $fntab->{$r->{type}};
	die unless defined $fn;
	&$fn($r);

	$r->{ofcall}->{tag} = $r->{ifcall}->{tag};
	$r->{ofcall}->{type} = $r->{ifcall}->{type}+1;
	if($r->{error}){
		$r->{ofcall}->{type} = Rerror;
		$r->{ofcall}->{ename} = $r->{error};
	}

	if($::chatty9p){
		print STDERR sprintf("-%d-> %s\n", fileno($srv->{iow}), fcallstr(%{$r->{ofcall}}));
		print STDERR Data::Dumper->Dump([$r], ['$r']) if $::chatty9p > 1;
	}

	writefcall($srv->{iow}, %{$r->{ofcall}})
		or die "writefcall: $@";
	$r->{responded} = 1;
#	%{$r} = ();
}

sub sversion {
	my ($r) = @_;

	if(substr($r->{ifcall}->{version}, 0, 2) ne "9P"){
		$r->{ofcall}->{version} = "unknown";
		respond($r, undef);
		return;
	}

	$r->{ofcall}->{version} = "9P2000";
	$r->{ofcall}->{msize} = $r->{ifcall}->{msize};
	respond($r, undef);
}

sub rversion {
	my ($r) = @_;

	$r->{srv}->{msize} = $r->{ofcall}->{msize};
}

sub sauth {
	my ($r) = @_;
	my $srv;

	$srv = $r->{srv};

	if(!defined($r->{afid} = _allocfid($srv, $r->{ifcall}->{afid}))){
		respond($r, "duplicate fid");
	}

	if($srv->{auth}){
		&{$srv->{auth}}($r);
	}else{
		respond($r, "authentication not required");
	}
}

sub rauth {
	my ($r) = @_;

	if($r->{error} and $r->{afid}){
		_removefid($r->{srv}, $r->{afid}->{fid});
	}
}

sub sattach {
	my ($r) = @_;
	my $srv = $r->{srv};

	if(!defined($r->{fid} = _allocfid($srv, $r->{ifcall}->{fid}))){
		respond($r, "duplicate fid");
	}

	$r->{afid} = undef;
	if($r->{ifcall}->{afid} != NOFID and !defined($r->{afid} = _lookupfid($srv, $r->{ifcall}->{afid}))){
		respond($r, "unknown fid");
	}

	$r->{fid}->{uid} = $r->{ifcall}->{uname};

	if($srv->{attach}){
		&{$srv->{attach}}($r);
	}else{
		respond($r, undef);
	}
}

sub rattach {
	my ($r) = @_;

	if($r->{error} and $r->{fid}){
		_removefid($r->{srv}, $r->{fid}->{fid});
	}
}

sub sflush {
	my ($r) = @_;
	
	respond($r, undef);
}

sub rflush {
	my ($r) = @_;
}

sub sclunk {
	my ($r) = @_;

	if(!defined($r->{fid} = _removefid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
	}else{
		respond($r, undef);
	}
}

sub rclunk {
}

sub sremove {
	my ($r) = @_;

	if(!defined($r->{fid} = _removefid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	if($r->{srv}->{remove}){
		&{$r->{srv}->{remove}}($r);
	}else{
		respond($r, "remove prohibited");
	}
}

sub rremove {
}

sub swalk {
	my ($r) = @_;

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	if($r->{fid}->{omode} != -1){
		respond($r, "cannot clone open fid");
		return;
	}
	if($r->{ifcall}->{nwname} and !($r->{fid}->{qid}->{type}&QTDIR)){
		respond($r, "walk in non-directory");
		return;
	}
	if($r->{ifcall}->{fid} != $r->{ifcall}->{newfid}){
		if(!defined ($r->{newfid} = _allocfid($r->{srv}, $r->{ifcall}->{newfid}))){
			respond($r, "duplicate fid");
			return;
		}
		$r->{newfid}->{uid} = $r->{fid}->{uid};
	}else{
		$r->{newfid} = $r->{fid};
	}


	if($r->{srv}->{walk}){
		&{$r->{srv}->{walk}}($r);
	}else{
		respond($r, "no walk function");
	}
}

sub rwalk {
	my ($r) = @_;
	my $wqid;

	$wqid = $r->{ofcall}->{wqid};
	if(!defined $wqid){
		$wqid = [];
		$r->{ofcall}->{wqid} = $wqid;
	}

	if($r->{error} or @{$wqid} < @{$r->{ifcall}->{wname}}){
		if($r->{newfid} and $r->{ifcall}->{fid} != $r->{ifcall}->{newfid}){
			_removefid($r->{srv}, $r->{newfid}->{fid});
		}
		if(@{$wqid}==0){
			if(!defined($r->{error}) and @{$r->{ifcall}->{wname}}!=0){
				$r->{error} = "file not found";
			}
		}else{
			$r->{error} = undef;	# No error on partial walks
		}
	}else{
		if(@{$wqid} == 0){
			# Just a clone
			$r->{newfid}->{qid} = $r->{fid}->{qid};
		}else{
			$r->{newfid}->{qid} = ${$wqid}[@{$wqid}-1];
		}
	}
}

sub screate {
	my ($r) = @_;

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	if($r->{fid}->{omode} != -1){
		respond($r, "9P protocol botch");
		return;
	}
	if(!($r->{fid}->{qid}->{type}&QTDIR)){
		respond($r, "create in non-directory");
		return;
	}

	if($r->{srv}->{create}){
		&{$r->{srv}->{create}}($r);
	}else{
		respond($r, "create prohibited");
	}
}

sub rcreate {
	my ($r) = @_;

	if($r->{error}){
		return;
	}
	$r->{fid}->{omode} = $r->{ifcall}->{mode};
	$r->{fid}->{qid} = $r->{ofcall}->{qid};
	if($r->{ofcall}->{qid}->{type}&QTDIR){
		$r->{fid}->{diroffset} = 0;
	}
	if($::chatty9p){
		print STDERR sprintf("fid mode is 0x%x\n", $r->{fid}->{omode});
	}
}

sub sopen {
	my ($r) = @_;

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	if($r->{fid}->{omode} != -1){
		respond($r, "9P protocol botch");
		return;
	}
	if($r->{fid}->{qid}->{type}&QTDIR and ($r->{ifcall}->{mode}&~ORCLOSE) != OREAD){
		respond($r, "is a directory");
		return;
	}
	$r->{ofcall}->{qid} = $r->{fid}->{qid};

	if($r->{srv}->{open}){
		&{$r->{srv}->{open}}($r);
	}else{
		respond($r);
	}
}

sub ropen {
	my ($r) = @_;

	if($r->{error}){
		return;
	}
	$r->{fid}->{omode} = $r->{ifcall}->{mode};
	$r->{fid}->{qid} = $r->{ofcall}->{qid};
	if($r->{ofcall}->{qid}->{type}&QTDIR){
		$r->{fid}->{diroffset} = 0;
	}
	if($::chatty9p){
		print STDERR sprintf("fid mode is 0x%x\n", $r->{fid}->{omode});
	}
}

sub sread {
	my ($r) = @_;
	my ($o, $z);

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	$o = $r->{fid}->{omode}&3;
	if($o != OREAD and $o != ORDWR and $o != OEXEC){
		respond($r, "not opened for reading");
		return;
	}
	if($r->{ifcall}->{count} < 0){
		respond($r, "9P protocol botch");
	}
	if($r->{ifcall}->{count} > $r->{srv}->{msize} - IOHDRSZ){
		$r->{ifcall}->{count} = $r->{srv}->{msize} - IOHDRSZ;
	}

	if($r->{ifcall}->{offset} < 0
	or (($r->{fid}->{qid}->{type}&QTDIR) and $r->{ifcall}->{offset} != 0 and $r->{ifcall}->{offset} != $r->{fid}->{diroffset})){
		respond($r, "bad offset");
		return;
	}

	if($r->{srv}->{read}){
		&{$r->{srv}->{read}}($r);
	}else{
		respond($r, "no read function");
	}
}

sub rread {
	my ($r) = @_;

	if($r->{error}){
		return;
	}
	if($r->{fid}->{qid}->{type}&QTDIR){
		$r->{fid}->{diroffset} += length($r->{ofcall}->{data});
	}
}

sub swrite {
	my ($r) = @_;
	my ($o, $z);

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	$o = $r->{fid}->{omode}&3;
	if($o != OWRITE and $o != ORDWR){
		respond($r, "not opened for writing");
		return;
	}
	if($r->{ifcall}->{offset} < 0){
		respond($r, "bad offset");
		return;
	}
	if($r->{ifcall}->{count} < 0){
		respond($r, "9P protocol botch");
	}
	if($r->{ifcall}->{count} > $r->{srv}->{msize} - IOHDRSZ){
		$r->{ifcall}->{count} = $r->{srv}->{msize} - IOHDRSZ;
	}

	if($r->{srv}->{write}){
		&{$r->{srv}->{write}}($r);
	}else{
		respond($r, "no write function");
	}
}

sub rwrite {
}

sub dirread9p {
	my ($r, $gen, @args) = @_;
	my ($d, $bits);
	local $_;
	
	if($r->{ifcall}->{offset} == 0){
		$_ = 0;
	}else{
		$_ = $r->{fid}->{dirindex};
	}
	while(defined ($d = &$gen(@args))){
		$bits = packdir(%$d);
		if(!defined $bits or length($bits)+length($r->{ofcall}->{data}) > $r->{ifcall}->{count}){
			last;
		}
		$r->{ofcall}->{data} .= $bits;
		++$_;
	}
	$r->{fid}->{dirindex} = $_;
}

sub readstr {
	my ($r, $str) = @_;
	my ($offset, $count);

	$offset = $r->{ifcall}->{offset};
	$count = $r->{ifcall}->{count};

	if($offset < length($str)){
		$r->{ofcall}->{data} = substr($str, $offset, $count);
	}else{
		$r->{ofcall}->{data} = '';
	}
}

sub sstat {
	my ($r) = @_;

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	if($r->{srv}->{stat}){
		&{$r->{srv}->{stat}}($r);
	}else{
		respond($r, "stat prohibited");
	}
}

sub rstat {
	my ($r) = @_;

	if($r->{error}){
		return
	};
	if(defined $r->{d}){
		$r->{ofcall}->{stat} = $r->{d};
	}
}

sub swstat {
	my ($r) = @_;

	if(!defined ($r->{fid} = _lookupfid($r->{srv}, $r->{ifcall}->{fid}))){
		respond($r, "unknown fid");
		return;
	}
	$r->{d} = $r->{ifcall}->{stat};
	if(!defined $r->{srv}->{wstat}){
		respond($r, "wstat prohibited");
		return;
	}
	if($r->{d}->{type} != $Plan9::syncdir{type}){
		respond($r, "wstat -- attempt to change type");
		return;
	}
	if($r->{d}->{dev} != $Plan9::syncdir{dev}){
		respond($r, "wstat -- attempt to change dev");
		return;
	}
	if($r->{d}->{qid}->{type} != $Plan9::syncdir{qid}->{type}
	or $r->{d}->{qid}->{vers} != $Plan9::syncdir{qid}->{vers}
	or $r->{d}->{qid}->{path} != $Plan9::syncdir{qid}->{path}) {
		respond($r, "wstat -- attempt to change qid");
		return;
	}
	if($r->{d}->{muid} ne $Plan9::syncdir{muid}){
		respond($r, "wstat -- attempt to change muid");
		return;
	}
	if($r->{d}->{mode} != $Plan9::syncdir{mode} and (($r->{d}->{mode}&DMDIR)>>24) != ($r->{fid}->{qid}->{type}&QTDIR)){
		respond($r, "wstat -- attempt to change DMDIR bit");
		return;
	}
	&{$r->{srv}->{wstat}}($r);
}

sub rwstat {
}

sub _allocfid($$) {
	my ($srv, $fid) = @_;
	my $f;

	if(exists $srv->{fpool}->{$fid}){
		return undef;
	}

	$f = {
		srv => $srv,
		fid => $fid,
		omode => -1,
	};
	$srv->{fpool}->{$fid} = $f;
	return $f;
}

sub _lookupfid($$) {
	my ($srv, $fid) = @_;

	return $srv->{fpool}->{$fid};
}

sub _removefid($$) {
	my ($srv, $fid) = @_;
	my $f;

	$f = $srv->{fpool}->{$fid};
	delete $srv->{fpool}->{$fid};
	return $f;
}

1;

