package SrcParser;

# This file is part of the build tools for Win32::GUI
# It encapsulates a set of functions to parse and retrieve
# documentation from within the source files.
#
# Author: Robert May , rmay@popeslane.clara.co.uk, 20 June 2005
# $Id: SrcParser.pm,v 1.1 2005/06/26 16:41:00 robertemay Exp $

use strict;
use warnings;

our $VERSION = "0.01";
our $DEBUG = 0;

# parse(@files)
# parses each file passed and "fixes" alternates that are found - see fix_alternates()
# return 1 on success
sub parse
{
  for my $file (@_) {
    parse_file($file);
  }

  fix_alternates();

  return 1;
}

# parse_file($file)
# parses the file, performing rudimentary error checking, and adding any documentation
# found to the stored data.
# dies on errors to force the documentation to be fixed
# returns 1 on success.

my %PACKAGES;
my %EVENTS;

sub parse_file
{
  my($file) = @_;
  print STDERR "Parsing file: '$file'\n" if $DEBUG;

  my $package;

  open(my $FILE, "<$file") or die "Can't open $file: $!";
  while(<$FILE>) {

    # Look for packages
    if(/\(\@\)PACKAGE:\s*(.*\S)\s*$/) {
      $package = $1;
      print STDERR "Found package: '$package'\n" if $DEBUG;

      # initialise packages structure if we haven't already seen the package
      if(not exists $PACKAGES{$package}) {
        $PACKAGES{$package} = {
                                files       => [],
                                methods     => {},
                                abstract    => '',
                                description => '',
                              };
      }

      # store the filename and linenumber where we found this package definition
      push @{$PACKAGES{$package}->{files}}, $file . "[$.]";

      # extract the package abstract and description
      # The abstract is on the line immediately following the package definition;
      # any remaining lines are package description;  We need to be careful, as
      # packages can be defined in multiple places.  Authors should take care to
      # only document the package once.  If there are multiple options, then
      # GUI.pm should be the preferred location
      my $abstract = <$FILE>;                # look at the next line
      if($abstract =~ s/^\s*(#|\*(\/)?)+\s*//) {  # if it looks like documentation, then store the abstact and description
        if($abstract) {
          die "Package $package (${file} [$.]) found a second abstract" if length $PACKAGES{$package}->{abstract} > 0;

          $PACKAGES{$package}->{abstract} = $abstract;

          # find the description:
          while(<$FILE>) {
            if( s/^\s*(#|\*(\/)?)+\s?// ) {  # if it looks like documentation, then store the description
              $PACKAGES{$package}->{description} .= $_;
            }
            else {
              last;
            }
          }
        }
      }
    }

    # Look for methods
    elsif(/\(\@\)METHOD:\s*(.*\S)\s*$/) {
      my $method = $1;
      $method =~ s/\((.*)\)//;  # strip the prototype
      my $methodprototype = $1; # and store it
      $method =~ s/\s+.*//;     # strip anything after the first space - copes with "new Win32::GUI::Thing(args)
      my $methoddescr = '';
      my @alternates;

      # We have an error if there's no current package
      die "Method $method (${file} [$.]) not in a package" if not defined $package;
      print STDERR "Found method: '${package}::${method}'\n\tPrototype: $methodprototype\n" if $DEBUG;

      # We have an error if the method has already been defined
      die "Method $method (${file} [$.]) already defined in package $package"
            if exists $PACKAGES{$package}->{methods}->{$method};

      # Look at the method description
      while(<$FILE>) {

        if(/\(\@\)METHOD:\s*(.*\S)\s*$/) {
          my $alternate = $1;
          $alternate =~ s/\(.*\)//;  # remove prototype
          $alternate =~ s/\s+.*//;
          push @alternates, $alternate;
          print STDERR "\tFound alternate method in package $package: '${alternate}'\n" if $DEBUG;
        }
        elsif( s/^\s*(#|\*(\/)?)+\s?// ) {
          $methoddescr .= $_;
        }
        else {
          # store the method details:
          $PACKAGES{$package}->{methods}->{$method} = {
                                                        prototype   => $methodprototype,
                                                        description => $methoddescr,
                                                        alternates  => \@alternates,
                                                      };
          last;
        }
      }
    }

    #Look for events
    elsif(/\(\@\)EVENT:\s*(.*\S)\s*$/) {
      my $event = $1;
      $event =~ s/\((.*)\)//;
      my $eventprototype = $1;
      $event =~ s/\s+.*//;
      my $eventdescr = '';
      my @packages;
      print STDERR "Found event: '$event'\n\tPrototype: $eventprototype\n" if $DEBUG;

      while(<$FILE>) {
        if(/\(\@\)APPLIES_TO:\s*(.*\S)\s*$/) {
          my $applies = $1;
          @packages = split(/\s*,\s*/, $applies);
          print STDERR "\tApplies to: $applies\n" if $DEBUG;
        }
        elsif( s/^\s*(#|\*(\/)?)+\s?// ) {
          $eventdescr .= $_;
        }
        else {
          # Store the event information
          if(scalar @packages == 0) {
            die "Event $event ($file) found that applies to no packages";
          }

          # store the event against each package it applies to
          else {
            for my $pack (@packages) {
              $pack = "Win32::GUI::" . $pack unless $pack eq '*';

              # The same evnet has multiple legitimate definitions in different packages
              # for the same package:
              # for example, Terminate() is described in both  Window.xs and MDI.xs,
              # applying to Win32::GUI::Window package in each case.  This is nasty to
              # document, but this is my best attempt:
              # - if the defining package and applies to package are the same store under
              #   the name of the event only.
              # - if they are not, append the defining package to the hash key so that there
              #   is no collision.
              my $frompackage = defined $package ? $package : $pack;

              my $tmpevent = $event;
              if ($frompackage ne $pack) {
                $tmpevent .= " ($frompackage)";
              }

              # it's an error if we've already seen the event
              die "Event $event (${file} [$.]) alredy defined in package($pack)" if exists $EVENTS{$pack}->{$tmpevent};

              # store the event info
              $EVENTS{$pack}->{$tmpevent} = {
                                            name        => $event,
                                            prototype   => $eventprototype,
                                            description => $eventdescr,
                                            file        => $file . "[$.]",
                                         };

            }
          }
          last;
        }
      }
    }

  }
  close($FILE);

  return 1;
}

# get_package_list()
# returns a sorted list of all the packages
sub get_package_list
{
  return sort { uc $a cmp uc $b } keys %PACKAGES;
}

# get_package_abstract(package)
# returns the abstract for a package
sub get_package_abstract
{
  my $package = shift;

  return $PACKAGES{$package}->{abstract};
}

# get_package_description(package)
# returns the description for a package
sub get_package_description
{
  my $package = shift;

  return $PACKAGES{$package}->{description};
}

# get_package_method_list(package)
# returns a sorted list of all the methods in a package
sub get_package_method_list
{
  my $package = shift;

  return sort newfirst keys %{$PACKAGES{$package}->{methods}};
}

# helper to sort methods: new method first, then alpha
sub newfirst
{
    return ($a =~ /^new/) ? -1 :
           ($b =~ /^new/) ? 1 : uc($a) cmp uc($b);
}

# get_package_method_prototype(package, method)
# returns the prototype of a method in a package
sub get_package_method_prototype
{
  my $package = shift;
  my $method = shift;

  return $PACKAGES{$package}->{methods}->{$method}->{prototype};
}

# get_package_method_description(package, method)
# returns the description of a method in a package
sub get_package_method_description
{
  my $package = shift;
  my $method = shift;

  return $PACKAGES{$package}->{methods}->{$method}->{description};
}

# get_common_events_list()
# returns a sorted list of all the global events
sub get_common_event_list
{
  return get_package_event_list('*');
}

# get_package_events_list(package)
# returns a sorted list of all the events associated with a package
sub get_package_event_list
{
  my $package = shift;

  return sort { lc $a cmp lc $b } keys %{$EVENTS{$package}};
}

# get_common_event_name(event)
# returns the name for the given common event
sub get_common_event_name
{
  my $event = shift;

  return get_package_event_name('*', $event);
}

# get_package_event_name(package, event)
# returns the name of a given event in a package
sub get_package_event_name
{
  my $package = shift;
  my $event = shift;

  return $EVENTS{$package}->{$event}->{name};
}

# get_common_event_prototype(event)
# returns the prototype for the given common event
sub get_common_event_prototype
{
  my $event = shift;

  return get_package_event_prototype('*', $event);
}

# get_package_event_prototype(package, event)
# returns the prototype of a given event in a package
sub get_package_event_prototype
{
  my $package = shift;
  my $event = shift;

  return $EVENTS{$package}->{$event}->{prototype};
}

# get_common_event_description(event)
# returns the raw description for the common event
sub get_common_event_description
{
  my $event = shift;

  return get_package_event_description('*', $event);
}

# get_package_event_description(package, event)
# return the raw description for an event in a package
sub get_package_event_description
{
  my $package = shift;
  my $event = shift;

  return $EVENTS{$package}->{$event}->{description};
}

# fix_alternates()
#  moves the alternate methods into the correct locakion, and adds text for them
# - if alternate is in same package, add it with same prototype
#   and description 'See thismethod()'
# - if package is different, add it with same prototype and description 
sub fix_alternates
{
  for my $package (keys %PACKAGES) {
    for my $method (keys %{$PACKAGES{$package}->{methods}}) {
      my $alternates = $PACKAGES{$package}->{methods}->{$method}->{alternates};

      my ($altpack, $altproto, $altdesc);
      for my $altmethod (@$alternates) {
        if ($altmethod !~ /^Win32::GUI::/) {
          $altpack = $package;
          $altproto = $PACKAGES{$package}->{methods}->{$method}->{prototype};
          $altdesc = "See $method()";
        }
        else {
          ($altpack = $altmethod) =~ s/(.*::)/$1/;
          $altproto = $PACKAGES{$package}->{methods}->{$method}->{prototype};
          $altdesc = $PACKAGES{$package}->{methods}->{$method}->{description} .
              "\n\n See also ${package}::${method}().";
        }

        die "alternate method ${altpack}::${altmethod} already defined."
            if exists $PACKAGES{$altpack}->{methods}->{$altmethod};

        # store away the details:
        $PACKAGES{$altpack}->{methods}->{$altmethod} = {
                                                        prototype   => $altproto,
                                                        description => $altdesc,
                                                      };
      }
    }
  }

  return 1;
}

1; # end of SrcParser
