#!/usr/bin/perl
# Filename:	scurvy
# Author:	David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License/
# Description:	Screenplay/screenwriting tool: txt->script formatter
# See:	http://screenplay.sourceforge.net/
use strict;

##################################################
# Setup the variables
##################################################
my $PROGNAME = $0;
$PROGNAME =~ s|.*/||;

##################################################
# Usage
##################################################
sub fatal {
  foreach my $msg (@_) { print STDERR "[$PROGNAME] ERROR:  $msg\n"; }
  exit(-1);
}

sub usage {
  foreach my $msg (@_) { print STDERR "ERROR:  $msg\n"; }
  print STDERR "\n";
  print STDERR "Usage:\t$PROGNAME [-d] <file>\n";
  print STDERR "\tFormats a script\n";
  print STDERR "\t-d\tSet debug mode\n";
  print STDERR "\t-c\tCount headings\n";
  print STDERR "\t-C\tShow \"Continued\" page breaks\n";
  print STDERR "\t-i\tAdd initial indent\n";
  print STDERR "\t-n\tShow page/line numbers\n";
  print STDERR "\n";
  exit -1;
}

sub parse_args {
  my %opt;

  # Defaults
  $opt{per_page} = 53;

  while (my $arg=shift(@ARGV)) {
    if ($arg =~ /^-h$/) { usage(); }
    if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; }
    if ($arg =~ /^-c$/) { $opt{count_head}=1; next; }
    if ($arg =~ /^-C$/) { $opt{page_breaks}=1; next; }
    if ($arg =~ /^-i$/) { $opt{indent}=1; next; }
    if ($arg =~ /^-n$/) { $opt{num}=1; next; }
    if ($arg =~ /^-/) { usage("Unknown option: $arg"); }
    usage("Too many files specified [$arg and $opt{file}]") if $opt{file};
    $opt{file}=$arg;
  }
  usage("No file defined") unless $opt{file};

  \%opt;
}

sub debug {
  return unless $MAIN::DEBUG;
  foreach my $msg (@_) { print STDERR "[$PROGNAME] $msg\n"; }
}

##################################################
# Main code
##################################################
my $HEADING	= 0;
my $ACTION	= 2;
my $DIALOGUE	= 3;	# array of [who,parenthetical,dialogue]
my $TRANSITION	= 4;
my $GENERAL	= 5;

sub read_script {
  my ($file) = @_;
  open(FILE,"<$file") || usage("Couldn't open file: $file");

  # Do something to the file
  my @script;
  my %alias;
  while(<FILE>) {
    chomp;
    next unless /\S/;
    next if /^#/;	# "post-notes" for comments
    last if /^ZZSTOP$/;	# hook for debugging scripts

    # Handle {aliases}
    s/{([^}\s]+)}/$alias{$1} || "{$1}"/eg;

    if (/^(\S+):=(\S[^\t]*)(\t.*)?$/) {
      $alias{$1}=$2;
    } elsif (/^(ext|int)/i) {
      push(@script, [$HEADING, uc($_)]);
    } elsif (/([^\t]+?)(\s*\(.+\))?:\t(?:\((.+)\)\s)?(\S.+)/) {
      my ($name, $vo, $paren, $txt) = ($1, $2, $3, $4);
      $name = uc($alias{$name} || $name);
      push(@script, [$DIALOGUE, "$name$vo", $paren, $txt]);
    } elsif (/^\t(\S.*)/) {
      push(@script, [$ACTION, $1]);
    } elsif (/^\t\t(\S.*)/) {
      push(@script, [$TRANSITION, uc($1)]);
    } else {
      push(@script, [$GENERAL, $_]);
    }
  }
  close(FILE);
  \@script;
}

sub fold {
  my ($cols, $txt, $pre, $pre2) = @_;
  $pre2 = $pre2 || $pre;

  my @fold;
  my $at = 0;
  my $line;
  while ($txt && $txt =~ s/^(\S*)(\s*)//) {
    my ($next,$space) = ($1,$2);
    my $l = length($next);
    my $ls = length($space);
    if ($at+$l+$ls < $cols) {
      $line .= $next.$space;
      $at+=$l+$ls;
    } elsif ($at+$l < $cols) {
      push(@fold, $line.$next);
      $line=""; $at=0;
    } elsif ($l > $cols) {
      push(@fold, $line.substr($next,0,$cols-$at));
      while (length($next) > $cols) {
        push(@fold, substr($next,0,$cols, ""));
      }
      $line = $next;
      $at = length($next);
      if ($at+$ls < $cols) {
        $line.=$space;
        $at+=$ls;
      } else {
        push(@fold, $line);
        $line=""; $at=0;
      }
    } elsif ($l+$ls < $cols) {
      push(@fold, $line);
      $line=$next.$space;
      $at=$l+$ls;
    } else {
      push(@fold, $line, $next);
      $line=""; $at=0;
    }
  }
  push(@fold, $line) if $line;
  my $ret = $pre.join("\n$pre2",@fold);
  split("\n", $ret);
}

sub write_script {
  my ($opt,$script) = @_;

  my $head = 1;
  my $tabsize = 5;	# tab is 5 chars
  my $t = " "x$tabsize;
  # Indent is 2 tabs
  my $indent = $opt->{indent} ? 2*$tabsize : 0;
  $indent -= 3 if $indent && $opt->{num};
  $indent = " "x$indent;

  my $line = 1;
  my $page = 1;
  my @add;

  print "PAGE $page:\n";

  foreach my $set ( @$script ) {
    my $what = shift @$set;
    if ($what == $HEADING) {
      my $txt = $set->[0];
      $txt = "$head $txt" if $opt->{count_head};
      $head++;
      @add = ("", fold(61,$txt));
    } elsif ($what == $ACTION) {
      @add = ("", fold(61,$set->[0]));
    } elsif ($what == $GENERAL) {
      @add = fold(78,$set->[0]);
    } elsif ($what == $TRANSITION) {
      @add = fold(16,$set->[0],"$t"x8);
    } elsif ($what == $DIALOGUE) {
      my ($name,$paren,$txt) = (@$set);
      @add = ("", fold(38,$name,"$t"x4));
      push(@add, fold(24,"$paren)","$t$t$t(","$t$t$t ")) if $paren;
      push(@add, fold(35,$txt,"$t$t")) if $txt;
    }

    if ($opt->{page_breaks} && $line + $#add+1 > $opt->{per_page}) {
      print " "x50,"(CONTINUED)\n\nCONTINUED";
      print " PAGE $page" if $opt->{num};
      print ":\n";
      $line = 1;
      $page++;
    }

    foreach ( @add ) {
      printf "%2d ",$line if $opt->{num} && /\S/;
      printf "",$line if $opt->{num};
      print "$indent$_\n";
      $line++;
    }
  }
}

sub main {
  my $opt = parse_args();

  my $script = read_script($opt->{file});
  write_script($opt,$script);
}
main();

##################################################
# POD/man
##################################################

__END__

=pod
=head1 NAME

scurvy - Format scripts / screenplays

=head1 SYNOPSIS

B<scurvy> [S<I<-c|-C|-i|-n>>]

=head1 DESCRIPTION

scurvy converts text files in a simple format into proper screenplay
format.  It's something I wrote because I hate using snifty GUI editors
when I believe a text editor is all you need.

  "If you can't vi it, it sucks"

It takes a text file as input and outputs a screenplay.  More formats
may occur someday..

=head1 OPTIONS


=over 4


=item B<-c>

Number the scene headings (INT/EXT)

=item B<-C>

Show the "CONTINUED" page breaks

=item B<-i>

Add the left margin indentation.  (Good for final print)

=item B<-n>

Show page/line numbers

=back

=head1 FORMAT

There are five types of formats: heading, action, dialogue, transition, general.
Each type B<must> be on it's own line.  (Use I<:set wrap> in vi/vim to make it easier to edit)

=over 4

=item B<scene heading>

Scene headings are automatically recognized since they start with INT or EXT.

=item B<action>

Action lines start after one tab.

=item B<transition>

Transition lines start after two tabs

=item B<dialogue>

Dialogue follows the characters name, a colon and a tab.  Some examples:

  Dave:	I think we should go shopping!
  God (V.O.):	That's a bad idea, Dave
  Dave:	(pondering)	You're probably right.

Parentheticals go after the colon, but V.O., O.S. go before.

=item B<general>

Generals are just regular text not prefaced by tabs.

=item B<comments>

Any line that starts with a '#' character is ignored.

=back

=head1 ALIASES

Aliases for characters can be defined on any line:

  D:=Dave

And then they can be used as the character speaking dialogue:

  D:	I think we should go shopping!

Or in any line of text if inside {curly braces}

  God (V.O.):	That's a bad idea, {D}

=head1 EXAMPLE

Here's an example input file:

  D:=Dave (aliases for characters look like this)
  INT. SCENE HEADING - DAY
  	Actions have one tab
  		Transitions have two tabs
  General text is just plain text.
  Dave:	dialogue follows the ":<tab>"
  John (V.O.):	voice overs go before the :
  D:	(using an alias!)	And parentheticals go after!

=head1 BUGS

Garbage in, garbage out.

=head1 AUTHOR

David Ljung Madison <http://MarginalHacks.com/>

=cut

