The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
## @file
# (Enter your file info here)
#
# @copy 2007 MailerMailer LLC
# $Id: DocGenerator.pm 353 2007-08-14 17:46:43Z damjan $

## @class RWDE::Doxy::DocGenerator
# Note: This class is abstract.
# It makes calls to fields that do not exist.
# Please do not instantiate.

package RWDE::Doxy::DocGenerator;

use strict;
use warnings;

use Error qw(:try);

use RWDE::Exceptions;
use RWDE::Doxy::MethodData;

use constant COMMENT        => '#';    #Comments start with this character
use constant COMMAND_PREFIX => '@';    #Prefix of the command

use constant CHARACTER_SIZE => 1;      #Size of one character
use constant PARAM_SIZE     => 9;      #Size of '$$params{'
use constant RETURN_SIZE    => 6;      #Size of 'return'

## @method void doxygenate()
# Write/save the content to file. if you give a filename overwrites the original saves backup into the filename given
# this function takes the filename gets the contents and creates all the tags we know how returns the marked up contents
sub doxygenate() {
  my ($self, $params) = @_;

  #Process the file, placing all non_doxy lines into file_content_out
  #also populate MethodData and class data with the necessary info.
  $self->_create_intermediate_data();

  #We now	have intemediate data in file_content_out
  #We want to read from the file_content_out and write the complete doxygenated file
  #So place the file_content_out into the file_content_in.
  #Reset the file and class cursors.
  #Reset file content out
  $self->file_content_in($self->file_content_out);
  $self->reset_file_index();
  $self->reset_file_content_out();
  $self->reset_file_current_class();

  #Insert initial info on top of page
  $self->_insert_file_data();

  #Insert all other command and info
  $self->_insert_document_data();

  return ();
}

## @method protected void _create_intermediate_data()
# Form an array of intermediate data. This will consist of all lines in file_content_in that are not doxy_lines
# Also populate class and method data so we can preserve doxy comments.
# (We can then add them back in somewhere else)
sub _create_intermediate_data() {
  my ($self, $params) = @_;

  #Loop through every line of the file and parse/analyze it.
  while ($self->current_line()) {

    #if it is a doxy command tag

    #enter if the current line is a doxy tag command
    if ($self->current_line() =~ /^\s*## @(\S+)\s?(.*)(\n)/) {
      my $command_name = $1;
      my $command_info = $2;

      #Already saw the command. We will not need it again so increment the file index
      #We will regenerate this line when we remake the doxy tags so this will not be added to file_content_out
      $self->next_line();

      if ($command_name eq 'file') {
        $self->_store_file_info();
      }
      elsif ($command_name eq 'class') {

        #Command info consists of the class name
        #Ex:
        #@class <class_name>
        #@class is the $command_name, whicle the class name is the $command_info,
        $self->_store_class_info({ command_info => $command_info });

        #Methods will need to know what class they belong to
        #We will keep the name of this class handy so we can store the methods' method_data in the correct class_data
        $self->file_current_class($command_info);
      }
      elsif ($command_name eq 'method' or $command_name eq 'cmethod') {
        $self->_store_method_info({ command_info => $command_info });
      }
      else {

        #This tag could be a todo, enum, etc. So just echo it to file_out
        $self->add_to_file_out({ content => $self->current_line() });
        $self->next_line();
      }
    }

    #This is not a tag so just add the line to the end of file_content_intermediate
    else {
      $self->add_to_file_out({ content => $self->current_line() });
      $self->next_line();
    }
  }
}

## @method protected void _store_file_info()
# Store all \@file-related doxy info lines (no command present) for later use.
# All \@file info is placed at the top of the perl document file.
sub _store_file_info() {
  my ($self, $params) = @_;

  $self->_store_info({ data => $self });
}

## @method protected void _store_class_info($command_info, $command_info})
# Store all \@class-related doxy info lines (no command present) for later use.
# @param command_info  (Enter explanation for param here)
sub _store_class_info() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No class_name specified' })
    unless defined($$params{command_info});

  $self->_store_info({ data => $self->get_class({ class_name => $$params{command_info} }) });

  return ();
}

## @method protected void _store_method_info($command_info)
# Store all \@method-related doxy info lines (no command present) for later use.
# @param command_info  (Enter explanation for param here)
sub _store_method_info() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No class_name specified' })
    unless defined($$params{command_info});

  my $class_data = $self->get_class({ class_name => $self->file_current_class });

  $$params{command_info} =~ /(\S+)\(.*\)\s*$/;

  my $method_data = $class_data->get_method({ method_name => $1 });

  $self->_store_info({ data => $method_data, sub_info => 1 });

  return ();
}

## @method protected void _store_info($data)
# Checks to see if a class info exists
# If a ClassData entry exists then tell it to store the info lines
# If it does not exist then create a new ClassData object and populate the info
# @param data  (Enter explanation for param here)
sub _store_info() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No data specified' })
    unless defined($$params{data});

  my $data = $$params{data};

  #While we are looking at a "block" of doxy tags (every line until we do not start a line with "# ")
  while ($self->current_line() =~ /^\s*#\s(.*)\n/) {

    #@ implies that a tag exists so ignore anything that is not an info line
    #However, we will need to store params, etc.
    if ($self->current_line() !~ /^\s*#\s\@(\S+)\s*(.*)/) {
      $data->add_info({ info_line => $1 });
    }
    else {

      #Files and classes do not have params and returns.
      #But methods do 
      #This area will only be intered if sub_info param is defined.
      if (defined($$params{sub_info})) {
        if ($1 eq 'param') {
          my $param_info = $2;

          $param_info =~ /(\S+)\s*(.*)/;

          $data->add_param_info({ param_name => $1, param_info => $2 });
        }
        elsif ($1 eq 'return') {
          $data->set_return_info({ return_info => $2 });
        }
      }
    }

    $self->next_line();
  }

  return ();
}

## @method protected void _insert_file_data()
#Insert \@file template info at the top of the file
#Four lines, where filename is the file_name of the file.
#		## \@file
#		# (Enter your file info here)
#		#
#		# \@copy 2007 MailerMailer LLC
#		# Subversion ID tag
sub _insert_file_data() {
  my ($self, $params) = @_;

  my $info_lines = $self->file_info();

  #Create the entire tag for this command
  $self->add_to_file_out({ content => $self->_command({ command => 'file' }) });

  #Want to check if we store any @file lines
  if (scalar @{$info_lines} > 0) {

    #We have some lines stored. Add them in
    $self->_insert_info_lines({ info_lines => $info_lines });
  }
  else {

    #No lines were found so generate the default stuff.
    $self->add_to_file_out({ content => $self->_info({ info => '(Enter your file info here)' }) });
    $self->add_to_file_out({ content => $self->_info() });
    $self->add_to_file_out({ content => $self->_sub_command({ command => 'copy', info => '2007 MailerMailer LLC' }) });
    $self->add_to_file_out({ content => $self->_info({ info => '$Id$' }) });
    $self->add_to_file_out({ content => "\n" });
  }

  return ();
}

## @method protected void _insert_document_data()
#Insert method and sub tag data into new updated_file_data
#The methods inside the 'if' will increment the line_index and return it.
sub _insert_document_data() {
  my ($self, $params) = @_;

  #Loop through every line of the file and parse/analyze it.
  while ($self->current_line()) {

    #Does this line contain the start of a valid package/class?
    if ($self->current_line() =~ /^\s*package\s+(\S+);/) {

      #Update the current package name
      $self->file_current_class($1);

      #Insert class/package data before 'package <package_name> ...'
      $self->_insert_class_data({ class_name => $self->file_current_class });

      $self->add_to_file_out({ content => $self->current_line() });

      $self->next_line();
    }

    #Does this line contain a valid method.
    elsif ($self->current_line() =~ /^\s*sub\s+(\w+)/) {

      #Store current line index. we will need it later to copy lines to updated_file_content
      my $initial_line_index = $self->file_index;

      #Get the method data object or create a new one.
      my $class_data = $self->get_current_class();
      my $method_data = $class_data->get_method({ method_name => $1 });

      #Find method information (such as params, return info)

      #This method will increment $self->file_index so we will need it after the method is finished.
      $self->_determine_method_attributes({ method_data => $method_data });

      #Insert method/sub data before 'sub <sub_name> ...'
      $self->_insert_method_data({ method_data => $method_data });

      #Because the tag/comments occur on top of the method, we had to hold off until after that stuff was
      #taken care of. Now we can copy over the method, itself.
      #We subtract one off file_index because it is pointing to the line after the method, which
      #this loop will take care of on the next iteration.
      $self->add_range_to_file_out({ start_index => $initial_line_index, end_index => $self->file_index - 1 });
    }

    #Nothing special about the line, just copy it over
    else {
      $self->add_to_file_out({ content => $self->current_line() });
      $self->next_line();
    }

  }

  return ();
}

## @method protected void _insert_class_data($class_name})
# Insert \@class template info above the "package <package_name>"
# Two lines:
#	## @class
#	# (Enter your class info here)
# @param class_name  (Enter explanation for param here)
sub _insert_class_data() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No class_name specified' })
    unless defined($$params{class_name});

  $self->add_to_file_out({ content => $self->_command({ command => 'class', info => $$params{class_name} }) });

  my $info_lines = $self->get_class($params)->class_info();

  #If we saved some info lines from before...add them in.
  if (scalar @{$info_lines} > 0) {
    $self->_insert_info_lines({ info_lines => $info_lines });
  }
  else {

    #We have no lines to add so just generate an info line
    $self->add_to_file_out({ content => $self->_info({ info => '(Enter ' . $$params{class_name} . ' info here)' }) });
  }

  return ();
}

## @method protected object _insert_method_data($method_data)
# Insert method or cmethod template info above the "sub <sub_name> ..."
# @param method_data  (Enter explanation for param here)
sub _insert_method_data() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No method_data specified' })
    unless defined($$params{method_data});

  my $method_data = $$params{method_data};

  #Combine the method info
  $self->add_to_file_out({ content => $self->_command({ command => $method_data->method_type, info => $method_data->method_description() }) });

  if (scalar @{ $method_data->method_info } > 0) {
    $self->_insert_info_lines({ info_lines => $method_data->method_info });
  }
  else {

    #We have no lines to add so just generate an info line
    $self->add_to_file_out({ content => $self->_info({ info => '(Enter ' . $method_data->method_name . ' info here)' }) });
  }

  foreach my $param_name (keys %{ $method_data->method_params() }) {

    #Get param info for this param name. It will return undef if one does not exist.
    my $param_info = $method_data->get_param_info({ param_name => $param_name });

    #Use the param info if it exists or generate a generic info string if it does not
    $param_info = ($param_info ne undef) ? $param_info : '(Enter explanation for param here)';

    $self->add_to_file_out({ content => $self->_sub_command({ command => 'param', info => "$param_name  $param_info" }) });
  }

  if ($method_data->method_return_type ne "void") {
    my $return_info = ($method_data->method_return_info ne undef and $method_data->method_return_info ne '') 
    ? $method_data->method_return_info() : '(Enter explanation for return value here)';

    $self->add_to_file_out({ content => $self->_sub_command({ command => 'return', info => $return_info }) });
  }

  return ();
}

## @method protected void _insert_info_lines($info_lines)
# (Enter _insert_info_lines info here)
sub _insert_info_lines() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No info_lines specified' })
    unless defined($$params{info_lines});

  foreach my $info_line (@{ $$params{info_lines} }) {
    $self->add_to_file_out({ content => $self->_info({ info => $info_line }) });
  }

  return ();
}

## @method protected object _determine_method_attributes($method_data)
# Add the new tag lines to the updated_file_content
# @param method_data  (Enter explanation for param here)
sub _determine_method_attributes() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No method_data specified' })
    unless defined($$params{method_data});

  my $method_data = $$params{method_data};

  #We always need to go into the while loop once
  my $initial_loop = 1;

  #For every { we will increment the bracket count by 1. For every } we will decrement the bracket count by 1.
  #Once the bracker count reaches zero, then we have found the end of the method.
  #Start by incrementing here if necessary because one { could be on the same line as the method declaration: "sub my_name() {"
  #But we only want to do that if one is there.
  if ($self->current_line() =~ /\{/) {
    $method_data->increment_brackets_count();
  }

  #Go to the next line and start pulling information from the method
  $self->next_line;

  #Loop through the entire line (which is a string) and look for certain properties.
  while (($self->current_line() and ($method_data->method_brackets_count != 0)) or $initial_loop) {
    $initial_loop = undef;

    #The character index for this line's string
    my $line             = $self->current_line();
    my $char_index       = 0;
    my $no_comment_found = 1;                       #True as long as we do not find a comment

    while ($char_index < length($line) and $no_comment_found) {

      #If this character is a comment then the rest of the lines should not be analyzed.
      if (substr($line, $char_index, CHARACTER_SIZE) eq COMMENT) {
        $no_comment_found = 0;
      }

      #Is this character a {. If so then increment the bracket counter
      elsif (substr($line, $char_index, CHARACTER_SIZE) eq "{") {
        $method_data->increment_brackets_count();
        $char_index++;
      }

      #If this character is a } then decrement the bracket counter
      elsif (substr($line, $char_index, CHARACTER_SIZE) eq "}") {
        $method_data->decrement_brackets_count();
        $char_index++;
      }

      #Do the next 9 characters show '$$params{'. If so, then we have found a param (unless a compile problem exists).
      #There is one case where we do not want to count $$params{<something>} as a parameter
      #-Lets say the we assign a value to a param key, such as $$params{something} = 4;
      #-This param was not passed in, so we do not want to count it
      #-However, if the param was passed in and it is reassigned (as above), it will still get picked up as a param
      #because it will appear at least twice.
      #-The regex takes care of this.
      elsif (substr($line, $char_index, PARAM_SIZE) eq '$$params{' and $line !~ /\$\$params\{\s*\S+\s*}\s*=/) {
        my $param_name;
        my $rest_of_line = substr($line, $char_index + PARAM_SIZE, length($line) - $char_index + PARAM_SIZE);

        if ($rest_of_line =~ /^\s*(\w+)/) {
          $param_name = $1;
          $method_data->add_param({ param_name => $param_name });
          $char_index += (PARAM_SIZE + length($param_name) + 1);    #The +1 is for the '}'
        }
        else {
          throw RWDE::DataBadException({ info => 'Malformed param File will probably not compile. On line ' . $self->file_index });
        }
      }
      elsif (substr($line, $char_index, RETURN_SIZE) eq 'return') {
        my $rest_of_line = substr($line, $char_index + RETURN_SIZE, length($line) - $char_index + RETURN_SIZE);

        #If not "return;" and not "return();" then it is an object. Otherwise leave it at the default void.
        if ($rest_of_line !~ /^\s*\(?\s*\)?;/){
          $method_data->method_return_type('object');
        }

        $char_index += RETURN_SIZE;
      }
      else {
        $char_index++;
      }
    }

    $self->next_line();
  }

  return ();
}

## @method protected object _command($command)
# This returns a one-line command string.
# Example:	command param is 'file', info param is "info"
#					will return '## @file info'
# @param command  (Enter explanation for param here)
# @return (Enter explanation for return value here)
sub _command() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'Command not specified' })
    unless defined($$params{command});

  #Use two comment characters for the major command.
  $$params{prefix} = COMMENT . COMMENT;

  return $self->_line($params);
}

## @method protected object _sub_command($command)
#This returns a one-line sub command string.
#Example:	command param is 'file', info param is 'info"
#					will return '# @file info'
# @param command  (Enter explanation for param here)
# @return (Enter explanation for return value here)
sub _sub_command() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'Command not specified' })
    unless defined($$params{command});

  $$params{prefix} = COMMENT;

  return $self->_line($params);
}

## @method protected object _info($command)
# This returns a one-line info string.
# Example:	info param is 'file'
#					will return '# file'
# Note that commands are not allowed in info strings. Please see _command and _sub_command for that
# @param command  (Enter explanation for param here)
# @return (Enter explanation for return value here)
sub _info() {
  my ($self, $params) = @_;

  #info does not have a command. So passing in a command is illegal.
  throw RWDE::DataMissingException({ info => 'Command specified' })
    unless not defined($$params{command});

  $$params{prefix} = COMMENT;

  return $self->_line($params);
}

## @method protected object _line($command, $info, $prefix)
# This returns a one-line sub command string.
# Example:	info param is 'file'
#					will return '# file'
# Note that commands are not allowed in info strings. Please see _command and _sub_command for that
# @param info  (Enter explanation for param here)
# @param command  (Enter explanation for param here)
# @param prefix  (Enter explanation for param here)
# @return (Enter explanation for return value here)
sub _line() {
  my ($self, $params) = @_;

  throw RWDE::DataMissingException({ info => 'No tag prefix specified' })
    unless defined($$params{prefix});

  my $tag = '';    #We just want an empty line for some reason.;

  if (defined($$params{info}) and defined($$params{command})) {
    $tag = COMMAND_PREFIX . $$params{command} . ' ' . $$params{info};
  }
  elsif (defined($$params{command})) {
    $tag = COMMAND_PREFIX . $$params{command};
  }
  elsif (defined($$params{info})) {
    $tag = $$params{info};
  }

  return $$params{prefix} . ' ' . $tag . "\n";
}

1;