#########section from template follows!!!!!!
#Project attributes. Feel fee to edit them if you 
my $indir = $prjinfo{indir};
my $outdir = $prjinfo{outdir};#directory to put output to.
my $statedir = $prjinfo{stdir};#directory with state files that are safe to loose
	    #since they are automatically autogenerated
my $title = $prjinfo{title};#mostly not used
my $testcmd = $prjinfo{testcmdline};#command to run for testing
my ($precmd,$postcmd)  =($prjinfo{precmdline}, $prjinfo{postcmdline});

#host attributes
my $obfuspath = $prjinfo{obfuspath};
my $obfusbin = $prjinfo{obfusbin};


my $symfiles_subdir = 'list-of-symbols-for-obfuscator';
my $idents_from_html_fname = 'idents-from-html.txt';
my $mapping_filename = 'symbol-mapping.txt';
my $shortest_counts_fname = 'shortest-countsfile.txt';
my $shortest_state_fname = 'shortest-statefile.txt';
my $tmp_found_libs_fname = 'tmp-found-libs';
my $fnmapping_fname = 'filename-mapping.txt';
my $dnmapping_fname = 'dirname-mapping.txt';
my $fname_predicate_script_fname = 'fname-predicate.pl';
my @statefnames = ($idents_from_html_fname,$mapping_filename,qw(),$fnmapping_fname,$dnmapping_fname,
    $shortest_counts_fname, $shortest_state_fname);
my $filelist_fname = 'tmp-filelist.txt';
my $strinfinfo_dirname = 'stringinfo';



#subs to construct paths relative to indir/statedir/outdir
my $have_filename_mangling = 0;
my @filename_mangling_cmdline_options_to_add = ();

sub rpi { File::Spec->catfile($indir,$_[0]); }
sub rps { File::Spec->catfile($statedir,$_[0]); }
sub rpo { my $rd = $have_filename_mangling ? fnmap::do_map($_[0]) : $_[0];
     File::Spec->catfile($outdir,$rd); }
sub rpy {
    my %userfile_props = qw(
		main 	user-exceptions.txt 
		maini 	user-exceptions-nocase.txt	
    		anti    user-antiexceptions.txt
		mapping user-specified-mapping.txt
		suffix	user-suffixes-asis.txt);
    my $type = shift;
    my $nm = $userfile_props{$type};
    my $r = File::Spec->catfile($indir,"$symfiles_subdir/$nm");
    $r;
}

sub pq { "\"$_[0]\""; } #returns quoted pathname

my $filelist_full_fname = pq(rps($filelist_fname));
my $verbose;

my $plainperlpath = $^X || $ENV{STUNNIXWS_PERL_INTERP};

sub fill_unassigned_files
{
    #unknownfiles_handling
    my %files_assigned;
    {#fill %files_assigned
	foreach my $rl (values %modes_to_files)
	{
	    map {  $files_assigned{$_} = 1; } @$rl;
	};
    }
    return if !$prjinfo{unknownfiles_handling};
    my @modespec = @{$prjinfo{unknownfiles_handling}};    
    return if !@modespec;

    my $dir_was = getcwd();
    use POSIX qw(getcwd);
    chdir($indir);

    my @allfiles;
    _list_all_files(".", \@allfiles, 0);

    #now check all files found
    my @files_found;

    my %dirs_found;
    my $is_on_windows = $^O =~ /Win32/i  ? 1 : 0;
    foreach my $pn (@allfiles)
    {
	$pn = substr($pn,1);

	next if $files_assigned{$pn};

	my $ext;
	$pn =~ m,[.]([^.]+)$,;
	$ext = $1 if length($1);
	next if !length($ext); #no extension
	my $mode_found;
	foreach(@modespec)
	{
	    my ($type,$pat,$mode) = @$_;
	    if ($is_on_windows && $type eq 'ext') #on windows, filenames are case-insensitive! So lowercase pattern and extension
	    {
		$pat = lc $pat;
		$ext = lc $ext;
	    };
	    if ($type eq 'ext')
	    {
		my %exts = ($pat,1);
		map { $exts{$_} = 1} split(/\s+/,$pat);
		if ($exts{$ext} || $exts{".$ext"})
		{
		    $mode_found = $mode;
		    last;
		};
	    } elsif ($type eq 'rx') {
		$mode_found = $mode if ($pn =~ /$pat/);
	    };
	};
	if ($mode_found) {
	    my $mode = $mode_found;
    	    $modes_to_files{$mode} = [] if !$modes_to_files{$mode};
	    push @{$modes_to_files{$mode}}, $pn;
	    push @files_found, $pn;

	    $pn =~ s,/[^/]+$,,g; #extract dirname
	    $dirs_found{$pn} = 1;
	};
    };
    my @dirs_found = keys %dirs_found;
    if (@dirs_found) { #populate @alldirs so that dirs with found files are created
	foreach (@dirs_found)
	{
	    my @dirchain = split(/\/+/, $_);
	    my $p = "";
	    foreach (@dirchain)
	    {
		$p .= $_ . '/';
		if ($p ne '/') {
		    push @alldirs,$p;
		}
	    }
	}
    };
    chdir $dir_was;
    @files_found;
}

my %excluded_dirs;
my @excluded_dirs_list = (qw(CVS .svn .git .hg));
@excluded_dirs{@excluded_dirs_list} = @excluded_dirs_list;


sub _list_all_files
{
    my ($dirnm,$listref, $depth) = @_;
    opendir(D,$dirnm);
    my @dirs;
    foreach (readdir(D)) {
	next if $_ eq '.' || $_ eq '..' || (($excluded_dirs{$_} ||
	    $_ eq 'list-of-symbols-for-obfuscator') && -d "$dirnm/$_");
	my $nm = "$dirnm/$_";
	push @dirs,$nm if -d $nm;
	push @$listref, $nm if -f $nm;
    };
    closedir(D);
    foreach (@dirs) { _list_all_files($_,$listref,$depth+1); }
};

sub run
{
    my ($flags,@args) = @_;
    @ARGV = @args;
    my ($op,$nostop); #from commandline
    $op = 'build';
    my @files;
    my $runcmderrcnt;
    {#specified on commandline
	use Getopt::Long;    
	my $help;
	return 1 if (!GetOptions('op=s',\$op, 'help', \$help,
	    'verbose+',\$verbose,	
	    'nostop',\$nostop,
	    #'force',\$force
	    ));
	my %op_ok;
	my @opnames_ok = qw(rebuildall rebuildall-and-test get-used-libraries
			build build-and-test clearout clearstate clearall);
	@op_ok{@opnames_ok} = @opnames_ok;
	if (!$op_ok{$op})
	{
	    _t("error: unknown value for '--op' encountered: $op");
	    return 1;
    	}
	if ($help)
	{
	    use Pod::Usage;
	    pod2usage(-verbose => 1);
	}
	@files = @ARGV;
    }
    if ($prjinfo{lang} eq 'perl' && $^O =~ /Win32/i)
    {
	#we have to compute path to perl.exe since $^X is typically "*/wperl.exe"
	$plainperlpath =~ s,([\\/])wperl.exe,\1perl.exe,i;
    };
    $plainperlpath = "\"$plainperlpath\" ";

    {
	my @added_files = fill_unassigned_files();
	if (@added_files)
	{
	    t("auto-added " . (0+@added_files) . " files to the project");
	    t2("their names are:\n " . join("\n",@added_files));
	};
    };
    chdir($indir);
    mkdir(rpo('')); #make output directory - it can be absent!
    mkdir(rps($strinfinfo_dirname)); #ensure it exists before we do anything

    my @fnames = @files;# everything we should try to process
    if (!@fnames)
    {
        map { push @fnames, @$_ } values %modes_to_files;
    } else {
	#fix fnames passed
	@fnames = map {
	    substr($_,0,1) eq '/' ? $_ : "/$_";
	    } @fnames;
    }
    my %fname_to_mode;#key is filename, value modeid; for files in question
    my @list_of_modes_with_fnmangling = fnmap::list_modes_that_need_fn_mangling(\%modes,\%modes_to_files);
    if (@list_of_modes_with_fnmangling)
    {
	$have_filename_mangling = 1;
	chdir($indir);    
	fnmap::init_mapping();

	fnmap::read_fn_mapping_from_file(rps($fnmapping_fname));
	fnmap::read_dirname_mapping_from_file(rps($dnmapping_fname));

	my @all_filenames;  map { push @all_filenames, @{$modes_to_files{$_}} } @list_of_modes_with_fnmangling;
	my @fns_to_generate = fnmap::check_fnreplacement_available_for_list(\@all_filenames);
	if (@fns_to_generate) {

	    my %mode_to_files_to_generate = (); #list of files for each mode that need new name
	    {#build %fname_to_mode only for files we need to rename
		my %fnames;
		@fnames{@fns_to_generate} = @fns_to_generate;
		foreach my $m (@list_of_modes_with_fnmangling)
		{	    
		    foreach my $fn (@{$modes_to_files{$m}})
		    {
			if (exists $fnames{$fn})    #we have to gen name for it
			{
			    $fname_to_mode{$fn} = $m;
			    $mode_to_files_to_generate{$m} = []  if (!$mode_to_files_to_generate{$m});
			    push @{ $mode_to_files_to_generate{$m} },  $fn;
			}
		    }					
		}
	    }


	    foreach my $m (@list_of_modes_with_fnmangling) {
		my $minfo = $modes{$m};
		my @list_of_fns = @{$mode_to_files_to_generate{$m}}; #list of fns to generate
		my $outfn = 'tmp-fnmapping-output.txt';
		create_filelist('',@list_of_fns); #now list is stored in rps($filelist_fname)

		my $cmdline = $plainperlpath .
		    pq(File::Spec->catfile($obfuspath,$obfusbin)) .
		    " $modes{$m}->{commandline} --generate-filenames " . rps($filelist_fname) . ' '. rps($outfn);
		if (-r rps($fname_predicate_script_fname)) {
		    $cmdline .= ' --filename-and-dirname-predicate-file ' . rps($fname_predicate_script_fname);
		};

		return t_out("no '$obfusbin' installed, aborting") if ! -r File::Spec->rel2abs($obfusbin,$obfuspath);

		my $errcnt=runcmd($cmdline);		
		fnmap::read_fn_mapping_from_file(rps($outfn));

		unlink(rps($outfn));
	    }
	    fnmap::save_fn_mapping_to_file(rps($fnmapping_fname));
	    %fname_to_mode = (); #reset it - it will be used later
	}

        #these options will be added to each invokation of obfuscator
        @filename_mangling_cmdline_options_to_add = (
    	    '--filename-mapping-file',rps($fnmapping_fname),
	    '--dirname-mapping-file',rps($dnmapping_fname),
	);

	fnmap::build_missing_dirname_mapping(\@alldirs);
        fnmap::save_dirname_mapping_to_file(rps($dnmapping_fname));

#fnmap::save_fn_mapping_to_file('/dev/stdout');
#fnmap::save_dirname_mapping_to_file('/dev/stdout');
    };

    if ($op eq 'clearall' || $op eq 'clearout')
    {	
	t( 'trying to erase ' .  (0+@fnames)  . " output files");
	t2(' - with names [' . join(', ',@fnames) . ']');
	chdir($indir);
	unlink map { rpo($_) } @fnames;
	unlink map { 
	    rps(File::Spec->catfile($strinfinfo_dirname,$_)) 
	} @fnames;
	
    }
    if ($op eq 'clearall' || $op eq 'clearstate')
    {	
	my @fnames = @statefnames;
	t( 'trying to erase ' .  (0+@fnames)  . " state files");
	chdir($indir);
	unlink map { rps($_) } @fnames;
    }
    return 0 if index($op,'clear') == 0;

    chdir($indir);
    if ($op =~ /build/)
    {
	runcmd_other($precmd);
    }    

    {#build %fname_to_mode
	my %fnames;
	@fnames{@fnames} = @fnames;
	foreach my $m (keys %modes_to_files)
	{	    
	    map { $fname_to_mode{$_} = $m if exists $fnames{$_} }  
		@{$modes_to_files{$m}};
	}
    }
    if ($op eq 'get-used-libraries')
    {
	#here we completely rebuild $fname_to_mode; %modes_to_files will never be touched
	$modes{_get_used_libraries} = {
		'title', 'special: get used libraries',
		'commandline', 		'',
		'handling_type',	'_protectcode',
		'nooutput',		1,
		'is_shortest_mangler' => 0,
                 'ident_extaction_type' => 'none'
	    };
	my %new_fname_to_mode;
	foreach my $fn (@fnames) 
	{ 
	    $new_fname_to_mode{$fn} = '_get_used_libraries' 
		if $modes{$fname_to_mode{$fn}}->{handling_type} eq '_protectcode';
	};
	%fname_to_mode = %new_fname_to_mode;
    }
    
    #create filter for comparing mtime of single file
    #initialize it with sub that marks all files as newer
    my $cb_grep_files_newer1 = sub { my ($f,$rlist) = @_; @$rlist; };
    if (index($op,'build')==0)
    {
	#set it to more inteligent filter that really checks mtime 
	$cb_grep_files_newer1 = sub { 
	    my ($f,$rlist) = @_;
	    my $fmtime = 0+(stat($f))[9];
	    grep { my $mt = 0+((stat(rpi($_)))[9]); $mt >= $fmtime; }
		 @$rlist;
	}
    }
    
    {#extract idents
	my @files_to_extr = grep { 
		length($modes{$fname_to_mode{$_}}->{ident_extaction_type}) &&
		($modes{$fname_to_mode{$_}}->{ident_extaction_type} ne 'none') } 
		keys %fname_to_mode;
	@files_to_extr = &$cb_grep_files_newer1(rps($idents_from_html_fname),
		    \@files_to_extr);	
	if (@files_to_extr)
	{
	    #we have some modes that require us to extact idents from html, do
	    #extraction now
	    chdir($indir);
	    my %files_to_extr;
	    map { $files_to_extr{$_} = $fname_to_mode{$_} }  @files_to_extr;
	    my $extrmode_to_files = group_by_key(\%files_to_extr);
	    while(my ($modenm,$filelist) = each %$extrmode_to_files)
	    {
		my $opts = $extractident_type_to_cmdline{
				$modes{$modenm}->{ident_extaction_type}}
			    . ' -l ' .  $filelist_full_fname;
		create_filelist('.',@$filelist);		
		my $cmdline = "$plainperlpath " . 
		    pq(File::Spec->rel2abs('get-idents-from-html.pl',$obfuspath)) .
		    " $opts  " .
		    rps($idents_from_html_fname);
		return t_out("no 'get-idents-from-html.pl' installed, aborting") 
		    if ! -r File::Spec->rel2abs('get-idents-from-html.pl',$obfuspath);
		t('extracting idents from ' . (0+@$filelist) . ' file(s) with options "' .
		    $opts . '"');
		t2(' - with names [' . join(', ',@$filelist) . ']');
		return 2 if ($runcmderrcnt+=runcmd($cmdline) && !$nostop);
	    }
	}
    }
    if ($op ne 'get-used-libraries')
    {#create directories
	chdir($indir);
	map { mkdir rpo($_); 
	    my @iprops = stat(rpi($_)); chmod($iprops[2],rpo($_)); } @alldirs;
    }
    my $num_modes_with_code = 0;
    my $num_modes_with_code_total = 0;#count of files that are in the project, irregarding
	    #whether they need to be rebuilt or not
    for(my $step=0;$step<2;++$step)
    {
	#process files. If using 'shortest' obfuscator, gather symbol counts
	#on step 0
	my $mode_to_files = group_by_key(\%fname_to_mode);
	my $check_mtimes = index($op,'build')==0;
	chdir($indir);
	while(my ($m,$files) = each %$mode_to_files)
	{
	    my @files_to_process;
	    foreach my $f (@$files)
	    {
		my ($inp,$outp) = (rpi($f),rpo($f));
		next if !-e $inp; #skip missing files
		my $skip = 0;
		if ($check_mtimes)
		{
		    my ($imt,$omt) = (0+(stat($inp))[9], 0+(stat($outp))[9]);
		    $skip = $omt > $imt;
		    t2($skip ? "will not rebuild '$f'" : "will rebuild '$f'");
		} else {
		    t2("rebuilding '$f' unconditionally");
		}
		$num_modes_with_code_total++;
		push @files_to_process, $f if !$skip;
	    }
    	    next if !@files_to_process;
	    create_filelist('.',@files_to_process);
	    my $cmdline = $plainperlpath .
		    pq(File::Spec->catfile($obfuspath,$obfusbin)) .
		    " $modes{$m}->{commandline} ";
	    return t_out("no '$obfusbin' installed, aborting") 
		    if ! -r File::Spec->rel2abs($obfusbin,$obfuspath);
	    t('rebuilding ' . (0+@files_to_process) . 
		    " file(s) with mode '$modes{$m}->{title}'")
		if ($step==1 || 
		    ($modes{$m}->{handling_type} eq '_protectcode' &&
		        $modes{$m}->{is_shortest_mangler}));

	    my $cbnm = "modeimpl$modes{$m}->{handling_type}";

	    my $cmdtail;
	    if ($op ne 'get-used-libraries')
	    {
		    my $cmd;
		    if (!($modes{$m}->{handling_type} eq '_protectcode' &&
			$modes{$m}->{is_shortest_mangler} && $step ==0)) {
		        $cmd .= " -d " . rps($mapping_filename) . ' ';
		    };
		    $cmd .= ' -x ' . rpy('main') if -f rpy('main');
		    $cmd .= ' -x ' . rps($idents_from_html_fname) 
					if -f rps($idents_from_html_fname);
		    $cmd .= ' -F ' . rpy('mapping') if -f rpy('mapping');
		    $cmd .= ' --excludeidentsfile-anycase ' . rpy('maini')
				if -f rpy('maini');
		    $cmd .= ' -X ' . rpy('anti') if -f rpy('anti');
		    $cmd .= ' --suffixes-asis-list ' . rpy('suffix')
				if -f rpy('suffix');
		    $cmd .= join(' ','',@filename_mangling_cmdline_options_to_add,'');
		    if ($prjinfo{exception_libs})
		    {
			foreach (keys %{$prjinfo{exception_libs}})
			{
			    $cmd .= " -x $_ ";
			}
		    }
		    if ($prjinfo{libs_used})
		    {
			foreach my $l (keys %{$prjinfo{libs_used}}) {
			    my $v = $prjinfo{libs_used}->{$l};
			    next if $v eq '-';
			    $cmd .= " --exceptions-for-library $v:$l ";
			}
		    };
		    $cmdtail = $cmd;
	    } else {
		$cmdtail = " --save-list-of-included-files " . rps($tmp_found_libs_fname);
	    }
	    my $handle_files_by_one = 1;
	    if ($prjinfo{lang} ne 'perl')
	    {
		my $noout_part = $modes{$m}->{nooutput} ? "nooutput=1,":'';
		$cmdtail .= " -S multifile,outdir=$prjinfo{outdir},${noout_part}filelist=$filelist_full_fname ";
		$handle_files_by_one = 0;
	    };
	    if ($modes{$m}->{handling_type} eq '_protectcode' &&
		    $modes{$m}->{is_shortest_mangler})
	    {
		$cmdtail .= ' -i  shortest,countsfile=' .
			rps($shortest_counts_fname) . 
			' -i shortest,statefile=' . 
			      rps($shortest_state_fname);
	    }
	    if ($modes{$m}->{handling_type} eq '_protectcode' &&
		    $modes{$m}->{is_shortest_mangler} && $step ==0)
	    {
		$cmdtail .= ' -i  shortest,countsfile=' .
			rps($shortest_counts_fname) . 
			' -i shortest,statefile=' . 
			      rps($shortest_state_fname);
		if (!$handle_files_by_one)
		{
		    t("gathering symbol counts");
		    my $cmd = "$cmdline $cmdtail -i shortest,countupdate=1 " .
			    "  -S multifile,nooutput=1 ";
		    return 5 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
		} else {
		    foreach my $f (@files_to_process)
		    {
			my ($inp,$outp) = (rpi($f),rpo($f));
			my $cmd = $cmdline . " -o " . rps('/dummyfile') . " " . rpi($f) .
			    $cmdtail . " -i shortest,countupdate=1 ";
			t2("cmdline is $cmd");
			t("  gathering symbol counts in $f");
			return 3 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
		    };
		}	
	    }
	    next if $step ==0;
	    
	    ++$num_modes_with_code 
		    if $modes{$m}->{handling_type} eq '_protectcode';
	    #now process files
	    if (!$handle_files_by_one && 
		    $modes{$m}->{handling_type} eq '_protectcode')
	    {
		$cmdtail .=  ' --stringinfo-output-to=' .  rps($strinfinfo_dirname)
		    if $prjinfo{string_explorer};

		my $ident = '     ';
		t("rebuilding following files at once:\n$ident" . 
		    join("\n$ident",@files_to_process) );
		my $cmd = "$cmdline $cmdtail ";
		return 6 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
	    } else {	    
		foreach my $f (@files_to_process)
		{
		    my ($inp,$outp) = (rpi($f),rpo($f));
		    if ($modes{$m}->{handling_type} ne 
			    '_protectcode') {
			no strict "refs";
			&$cbnm($f,$m,$inp,$outp);
		    } else {
			$cmdtail .=  ' --stringinfo-output-to ' .  File::Spec->catfile(rps($strinfinfo_dirname), $f)
			    if $prjinfo{string_explorer};

			my $cmd = $cmdline . " -o " . rpo($f) . " " . rpi($f) .
			    $cmdtail;
			t2("cmdline is $cmd");
			t("  rebuilding $f");
			return 4 if ($runcmderrcnt+=runcmd($cmd) && !$nostop);
		    };
		}
	    }
	}
    }
    if (!$num_modes_with_code_total)
    {
	t(<<EOT);
WARNING: no files were treated as containing code!! Probably your files have
non-standard file extensions. Please go to Settings ~ "For files", select files
of same type, select corresponding processing mode in the dropdown list,
and click "Assign Processing Mode" button.
EOT
    };
    if (!$runcmderrcnt && $op =~ /build/)
    {
	runcmd_other($postcmd);
    }
    if (!$runcmderrcnt && index($op,'and-test')>0)
    {
	runcmd_other($testcmd);
    }
    0;
}

our $logbuf;

sub _t
{
    my (@strs) = @_;
    print STDERR join('',@strs);
}

sub t
{
    my (@strs) = @_;
    _t('',@strs,"\n");
}

sub t2
{
    my (@strs) = @_;
    _t('',@strs,"\n") if $verbose>=2;
}

sub t_out
{
    my (@strs) = @_;
    _t('',@strs);
}

sub td
{
    my (@strs) = @_;
    _t('',@strs,"\n");    
}

#returns hash, key is one values in $h, value is ref to list of keys in 
#source hash
sub group_by_key
{
    my ($h) = @_;
    my %o = ();
    while(my ($k,$v) = each %$h)
    {
	$o{$v} = [] if (!exists $o{$v});
	push @{$o{$v}}, $k;
    };
    \%o;
}

sub runcmd_other
{
    my ($cmd) = @_;
    runcmd($cmd) if $cmd =~ /\S/;
}

sub runcmd
{
    my ($cmd) = @_;
    open(SAVEERR,">&STDERR");
    my $logfile = rps('logfile');
    open(STDERR,">$logfile");
    system($cmd);
    my $st = $? << 8;
    open(STDERR,">&SAVEERR");
    if (-s $logfile)
    {
	open(F,"<$logfile");
	my @lines = <F>;
	if ($lines[0] =~ /syntax OK\s*$/ && $prjinfo{lang} eq 'perl')
	{
	    shift @lines;
	}	
	close(F);
	t_out(@lines) if @lines;
    }
    $st;
}

sub modeimpl_copyasis
{ 
    my ($relname,$modeid,$innm,$outnm) = @_; 
    _docopyfile($innm,$outnm);
};

sub modeimpl_dontcopy
{
    my ($relname,$modeid,$innm,$outnm) = @_; 
    #do nothing
}

sub _docopyfile
{
    my ($innm,$outnm) = @_;
    open(IN,"<$innm") || return;
    open(OUT,">$outnm") || return;
    binmode(IN);
    binmode(OUT);
    my ($len,$buf);
    while (($len = read(IN,$buf,16*1024)) != 0)
    {
	print OUT $buf;
	$buf = '';
    }
    close(IN);
    close(OUT);
    
    my @iprops = stat($innm);
    chmod($iprops[2],$outnm);
}

sub create_filelist
{
    my ($pfx,@files_to_process) = @_;
    open(F1,">" . rps($filelist_fname));
    map { print F1 $pfx . "$_\n"; } @files_to_process;
    close F1; 
}


run({},@ARGV) if !$ENV{BS_DONT_INVOKE_RUN};


