#! /usr/bin/gawk -f
# Last edited on 2010-07-08 00:40:23 by stolfilocal

BEGIN {
  usage = ( ARGV[0] " [ -v junk=JUNKFILE ] < INFILE > OUTFILE" );
  abort = -1;
  # Reads a ".pov" or ".inc" file.
  # Removes all camera and light_source commands,
  # replacing them by a call to the "camlight" macro.
  # Writes the deleted text to "JUNKFILE" (default "junk.inc")
  if (junk == "") { junk = "junk.inc"; }
  cmtstate = 0;  # 0 = plain, 1 = inside "/*..*/"
  brastate = 0;  # 0 = plain, 1 = waiting opeing "{".
  povstate = 0;  # 0 = plain, 1 = inside "camera", 2 = "light source"
  hadcam = 0; # TRUE if any "camera" command was found.
  reset_camera_params();
  eols = 2; # Number of consecutive "end-of-line" characters just written. 
  
  # Default values:
  default_cam_ctr = "<0,0,0>";
  default_cam_vec = "<10,10,10>";
  default_cam_dst = "20.0";
  default_cam_rad = "10.0";
  
  # Output standard header:
  output_string("// Last edited on DATE TIME by USER\n");
  output_string("// Processed by remove-cam-lights\n");
}

function reset_camera_params()
{
  # Resets the camera parameters {cam_{ctr,loc,dir,sky,rot}}: 
  cam_ctr = "??"; # camera's "look_at"
  cam_loc = "??"; # camera's "location"
  cam_sky = "??"; # camera's "cam_sky"
  cam_dir = "??"; # camera's "direction"
  cam_rot = "??"; # camera's "rotation"
}  

(abort >= 0) { exit abort; }

// {
  lin = $0;
  # Line cleanup:
  gsub(/[\011]/, " ", lin);
  gsub(/[\015]/, "", lin);
  
  # Process line up to but not including the final "\n".
  # However we write "\n" to {junk} after each "*/",
  # at the end of each "//" comment, and at the end
  # of each camera/light specification.
  while (lin != "")
    { if (cmtstate == 1)
        { # inside "/*...*/" comment:
          if (match(lin, /[*][\/]/))
            { # Split {lin} just after the "*/":
              bef = substr(lin,1,RSTART+RLENGTH-1);
              lin = substr(lin,RSTART+RLENGTH);
              # Part before "*/" is junk:
              printf "%s\n", bef > junk;
              cmtstate = 0;
            }
          else
            { printf "%s", lin > junk; lin = ""; }
        }
      else if (brastate == 1)
        { # Expecting a "{" after "camera" or "light_source":
          if (match(lin, /^[ ]+/))
            { # Split blanks off, remain in same state:
              bef = substr(lin,1,RSTART+RLENGTH-1);
              lin = substr(lin,RSTART+RLENGTH);
              output_string(bef);
            }
          else if (match(lin, /^([\/][\/]|[\/][*]|[{])/))
            { # Get the matched string {key} and split {lin} at it:
              key = substr(lin,RSTART,RLENGTH);
              lin = substr(lin,RSTART+RLENGTH);
              if (key == "{")
                { printf "%s", key > junk; brastate = 0; }
              else if (key == "//")
                { printf "//%s\n", lin > junk; lin = ""; }
              else if (key == "/*")
                { printf "/*" > junk; cmtstate = 1; }
              else
                { prog_error("bad key"); }
            }
          else
            { data_error("missing \"{\" after camera or light_source"); } 
        }
      else if (povstate == 1)
        { # Inside "camera" command
          if (match(lin, /([\/][\/]|[\/][*]|[{}]|location|sky|look_at|rotate|direction)/))
            { # Get the matched string {key} and split {lin} at it:
              bef = substr(lin,1,RSTART-1);
              key = substr(lin,RSTART,RLENGTH);
              lin = substr(lin,RSTART+RLENGTH);
              # Part before {key} is always junk:
              printf "%s", bef > junk;
              if (key == "location")
                { printf "%s", key > junk; cam_loc = get_camera_param(); }
              else if (key == "sky")
                { printf "%s", key > junk; cam_sky = get_camera_param(); }
              else if (key == "look_at")
                { printf "%s", key > junk; cam_ctr = get_camera_param(); }
              else if (key == "rotate")
                { printf "%s", key > junk; cam_rot = get_camera_param(); }
              else if (key == "direction")
                { printf "%s", key > junk; cam_dir = get_camera_param(); }
              else if (key == "//")
                { printf "//%s\n", lin > junk; lin = ""; }
              else if (key == "/*")
                { printf "/*" > junk; cmtstate = 1; }
              else if (key == "}")
                { printf "}\n" > junk; povstate = 0; }
              else if (key == "{")
                { data_error("found \"{\" inside camera spec"); }
              else
                { prog_error("bad key"); }
            }
          else
            { printf "%s", lin > junk; lin = ""; }
        }
      else if (povstate == 2)
        { # Inside "light_source" directive
          if (match(lin, /([\/][\/]|[\/][*]|[{}])/))
            { # Get the matched string {key} and split {lin} at it:
              bef = substr(lin,1,RSTART-1);
              key = substr(lin,RSTART,RLENGTH);
              lin = substr(lin,RSTART+RLENGTH);
              # Part before {key} is always junk:
              printf "%s", bef > junk;
              if (key == "//")
                { printf "//%s\n", lin > junk; lin = ""; }
              else if (key == "/*")
                { printf "/*" > junk; cmtstate = 1; }
              else if (key == "}")
                { printf "}\n" > junk; povstate = 0; }
              else if (key == "{")
                { data_error("found \"{\" inside light source spec"); }
              else
                { prog_error("bad key"); }
            }
          else
            { printf "%s", lin > junk; lin = ""; }
        }
      else 
        { # Normal state ({povstate == 0, cmtstate == 0})
          if (match(lin, /^[ ]+/))
            { # Grab blanks and write them out:
              spc = substr(lin,RSTART,RLENGTH);
              lin = substr(lin,RSTART+RLENGTH);
              output_string(spc);
              printf "%s", spc > junk;
              # Retain current state
            }
          else if (match(lin, /^([\/][\/]|[\/][*]|camera|light_source)/))
            { # Get the matched string {key} and split {lin} at it:
              bef = substr(lin,1,RSTART-1);
              key = substr(lin,RSTART,RLENGTH);
              lin = substr(lin,RSTART+RLENGTH);
              # Print part before {key}:
              output_string(bef);
              # Decide next state
              if (key == "camera")
                { povstate = 1; brastate = 1;
                  if (hadcam) 
                    { data_warning("multiple cameras"); }
                  hadcam = 1;
                  reset_camera_params();
                  printf "%s", key > junk;
                }
              else if (key == "light_source")
                { povstate = 2; brastate = 1;
                  printf "%s", key > junk;
                }
              else if (key == "//")
                { printf "//%s\n", lin > junk; lin = ""; }
              else if (key == "/*")
                { cmtstate = 1; printf "/*" > junk; }
              else
                { prog_error("bad key"); }
            }
          else
            { output_string(lin); lin = ""; }
        }
    }
  # Process end-of-line:
  if ((povstate == 0) && (cmtstate == 0))
    { output_string("\n"); }
  else
    { printf "\n" > junk; }
}

function get_camera_param(   par,tag)
{
  # Extracts from the beginning of {lin} a camera parameter,
  # up to the next parameter keyword ("up", "right", etc.), comment,
  # or "}". Returns the parameter as result, writes the parsed
  # portion of {lin} to {junk}, and leaves the remainder in {lin}.
  # For now, require parameter to be on the same line as the keyword,
  # with no intervening comments.

  if (match(lin, /([\/][\/]|[\/][*]|[{}]|location|cam_sky|look_at|right|up|angle|rotate|direction)/))
    { # Get the matched string {tag} and split {lin} at it:
      par = substr(lin,1,RSTART-1);
      tag = substr(lin,RSTART,RLENGTH);
      lin = substr(lin,RSTART);
      if (tag == "{")
        { data_error("found \"{\" inside camera spec"); }
    }
  else
    { par = lin; lin = ""; }

  # The {par} string is junk:
  printf "%s", par > junk;
  # Cleanup the {par} string:
  gsub(/^[ ]+/, "", par);
  gsub(/[ ]+$/, "", par);
  gsub(/[ ][ ]+/, " ", par);
  gsub(/[ ][,]/, ",", par);
  gsub(/[,][ ]/, ",", par);
  gsub(/[ ][>]/, ">", par);
  gsub(/[<][ ]/, "<", par);
  # Check for missing parameter value:
  if (par == "")
      { data_error("missing camera parameter value"); return "??"; }
    else
      { return par; }
}
    
END {
  if (abort >= 0) { exit abort; }

  # Check for proper closing of things:
  if (cmtstate != 0)
    { param_error("unterminated \"/*...*/\" comment"); }
  else if (brastate != 0)
    { param_error("missing \"{\" after \"camera\" or \"light_source\""); }
  else if (povstate != 0)
    { param_error("unterminated camera or light source spec"); }
  
  if (hadcam == 0)
    { param_warning("no camera spec found"); }
  else
    { # Cleanup camera parameters, generate standard macro call:
    
      # Require {cam_sky} to be Y or Z:
      if (cam_sky == "??")
        { param_warning("missing camera \"sky\" parameter"); cam_sky = "y"; }
      else if (cam_sky !~ /^[yz]$/)
        { param_warning(("non-standard cam_sky \"" cam_sky "\""));
          cam_sky = ( "(" cam_sky ")" );
        }
      
      # Require {cam_ctr}, protect it:
      if (cam_ctr == "??")
        { param_warning("missing camera \"look_at\" parameter");
          cam_ctr = default_cam_ctr; 
        }
      else if (is_zero_vector(cam_ctr))
        { cam_ctr = "<0,0,0>"; }
      else if (! is_plain_vector(cam_ctr))
        { cam_ctr = ( "(" cam_ctr ")" ); }
      
      # Build the camera vector {cam_vec}:
      if (cam_loc != "??")
        { # Protect the camera location {cam_loc}:
          if (! is_plain_vector(cam_loc))
            { cam_loc = ( "(" cam_loc ")" ); }
          # Build the camera vector as {cam_loc - cam_ctr}:
          if (is_zero_vector(cam_ctr))
            { cam_vec = cam_loc; }
          else 
            { cam_vec = ( "(" cam_loc "-" cam_ctr ")" ); }
        }
      else if (cam_dir != "??")
        { # Take the camera direction as the camera vector:
          cam_vec = cam_dir; 
        }
      else
        { param_warning("missing camera \"location\" and \"direction\" parameter");
          cam_vec = "??";
        }

      # Rotate the camera if so specified:
      if (cam_rot != "??")
        { cam_vec = ( "vrotate(" cam_vec "," cam_rot ")" ); }
    
      # Print original camera parameters as comments: 
      output_string("\n");
      output_string("// Original camera parameters:\n");
      output_string(("// #local cam_ctr = " cam_ctr "\n"));
      output_string(("// #local cam_loc = " cam_loc "\n"));
      output_string(("// #local cam_vec = " cam_vec "\n"));
      output_string(("// #local cam_sky = " cam_sky "\n"));
      
      # Add standard camera spec: 
      cam_vec = default_cam_vec;
      cam_dst = estimate_cam_dst(unpov_vector(cam_ctr),unpov_vector(cam_loc));
      cam_rad = estimate_cam_rad(cam_dst);
      output_string("\n");
      output_string("#include \"camlight.inc\"\n");
      output_string(sprintf("camlight(%s,%s,%s,%s,%s,1.2)\n",cam_ctr,cam_rad,cam_vec,cam_dst,cam_sky));
    }
  close(junk);
}

function is_plain_vector(vec)
{ 
  # TRUE iff {vec} is a plain zero POV-Ray vector. 
  vec = cleanup_vector(vec);
  return (vec ~ /^[<][-+]?[0-9]+([.][0-9]*)?[,][-+]?[0-9]+([.][0-9]*)?[,][-+]?[0-9]+([.][0-9]*)?[>]$/);
}
 
function is_zero_vector(vec)
{ 
  # TRUE iff {vec} is a plain zero POV-Ray vector. 
  return is_plain_vector(vec) && (vec !~ /[1-9]/);
}
  
function cleanup_vector(vec)
{
  # Removes some superflouous spaces from a POV-Ray vector.
  # If the vector is plain, all spaces are removed and
  # numbers have at least one integer digit.

  # Remove allowed spaces:
  gsub(/^[ ]+[<]/,"<",vec);
  gsub(/[>][ ]+$/,">",vec);

  gsub(/[<][ ]+/,"<",vec);
  gsub(/[-][ ]+/,"-",vec);
  gsub(/[+][ ]+/,"+",vec);
  gsub(/[,][ ]+/,",",vec);
  
  gsub(/[ ]+[>]/,">",vec);
  gsub(/[ ]+[-]/,"-",vec);
  gsub(/[ ]+[+]/,"+",vec);
  gsub(/[ ]+[,]/,",",vec);

  # Make sure that numbers have a leading digit:
  gsub(/[-][.]/,"-0.",vec);
  gsub(/[+][.]/,"+0.",vec);
  gsub(/[,][.]/,",0.",vec);
  gsub(/[<][.]/,"<0.",vec);
  
  return vec;
}

function unpov_vector(vec)
{
  # Converts a POV-ray vector from POV-ray format "<{X},{Y},{Z}>" 
  # (plus spaces) to plain "{X} {Y} {Z}" (single infix spaces).
  # Returns "??" if {vec} is not in that format, or if the coords
  # are not explicit numbers.
  
  # Works only on plain 
  vec = cleanup_vector(vec);
  if (! is_plain_vector(vec))
    { param_warning(("invalid pov vector[" vec "]"));
      return "??";
    }
  
  # Replace POV-Ray demimiters by infix spaces:
  gsub(/[<>, ]+/, " ", vec);
  gsub(/^[ ]+/, "", vec);
  gsub(/[ ]+$/, "", vec);

  # Sanity check:
  if (vec !~ /^[-+]?[0-9]+([.][0-9]*)?[ ][-+]?[0-9]+([.][0-9]*)?[ ][-+]?[0-9]+([.][0-9]*)?$/)
    { prog_error(("unpov [" vec "]")); }

  return vec;
}

function estimate_cam_dst(cam_ctr,cam_loc,   n,ctr,loc,dx,xy,dz,d)
{
  # Tries to estimate the camera distance parameter from the final
  # camera interest point {cam_ctr} and location {cam_loc}. In case of
  # failure, returns a canventional positive distance.
  
  if (cam_ctr == "??") 
    { d = 0; }
  else if (cam_loc == "??") 
    { d = 0; }
  else
  { n = split(cam_ctr, ctr); if (n != 3) { prog_error(("bad ctr [" cam_ctr "]")); }
    n = split(cam_loc, loc); if (n != 3) { prog_error(("bad loc [" cam_loc "]")); }
    dx = ctr[1] - loc[1];
    dy = ctr[2] - loc[2];
    dz = ctr[3] - loc[3];
    d = sqrt(dx*dx + dy*dy + dz*dz);
  }
  if (d == 0)
    { return default_cam_dst; }
  else
    { return sprintf("%.3f", d); }
}

function estimate_cam_rad(cam_dst, r)
{
  # Tries to estimate the scene radius of interest from the final
  # camera distance parameter [cam_dst}. In case of
  # failure, returns a canventional positive distance.
  
  if (cam_dst == "??") { return "12.0"; }
  r = 0.55*(cam_dst + 0.0);
  if (r == 0)
    { return default_cam_rad; }
  else
    { return sprintf("%.3f", r); }
}

function output_string(str)
{
  # Writes {str} to {stdout}, supressing excess blank lines.
  # Assumes that {str} contains at most one "\n", at the end.
  if (str == "") { return; }
  if (str ~ /^[ ]*[\n]$/)
    { # Print "\n" only if prev line was not blank:
      if (eols < 2)  { printf "\n"; eols++ }
    }
  else
    { if (str !~ /^[^\n]+[\n]?$/) { prog_error("bad str") }
      # Remove any trailing blanks:
      gsub(/[ ]+[\n]/, "\n", str);
      # Print and update {eols}:
      printf "%s", str;
      eols = (substr(str, length(str), 1) == "\n" ? 1 : 0);
    }
}

function arg_error(msg)
{
  printf "** %s\n", msg > "/dev/stderr";
  abort = 1; exit abort;
}

function data_error(msg)
{
  printf "%s:%d: «%s»\n", FILENAME, FNR, $0 > "/dev/stderr";
  printf "%s:%d: ** %s\n", FILENAME, FNR, msg > "/dev/stderr";
  abort = 1; exit abort;
}

function data_warning(msg)
{
  printf "%s:%d: «%s»\n", FILENAME, FNR, $0 > "/dev/stderr";
  printf "%s:%d: warning: %s\n", FILENAME, FNR, msg > "/dev/stderr";
}

function param_error(msg)
{
  printf "%s:%d: ** %s\n", FILENAME, FNR, msg > "/dev/stderr";
  abort = 1; exit abort;
}

function param_warning(msg)
{
  printf "%s:%d: warning: %s\n", FILENAME, FNR, msg > "/dev/stderr";
}

function prog_error(msg)
{
  printf "%s:%d: ** program error: %s\n", FILENAME, FNR, msg > "/dev/stderr";
  abort = 1; exit abort;
}