#!/usr/bin/perl
#-------------------------------------------------------------------------------
# Extract documentation from Java source code.
# Philip R Brenan at gmail dot com, Appa Apps Ltd, 2017
#-------------------------------------------------------------------------------

package Java::Doc;
require v5.16.0;
use warnings FATAL => qw(all);
use strict;
use Carp qw(confess);
use Data::Dump qw(dump);
use Data::Table::Text qw(:all);
use POSIX qw(strftime);                                                         # http://www.cplusplus.com/reference/ctime/strftime/

our $VERSION = '20171006';

genLValueHashMethods(qw(parse));                                                # Combined parse tree of all the input files read
genLValueHashMethods(qw(classes));                                              # Classes encountered in all the input files read

my %veryWellKnownClassesHash = map {$_=>1} split /\n/, &veryWellKnownClasses;       # Urls describing some well known Java classes

sub urlIfKnown($$)                                                              # Replace a well known type with an informative url
 {my ($javaDoc, $type)  = @_;                                                   # Java doc builder, type to replace with an informative url

  for (keys %veryWellKnownClassesHash, @{$javaDoc->wellKnownClasses})
   {return qq(<a href="$_">$type</a>) if m(/$type.html);
   }

  $type
 }

sub parseJavaFile($$)                                                           # Parse java file for package, class, method, parameters
 {my ($javaDoc, $fileOrString)  = @_;                                           # Java doc builder, Parse tree, java file or string of java to process
  my $parse = $javaDoc->parse;                                                  # Parse tree

  my $s = $fileOrString =~ m(\n)s ? $fileOrString : readFile($fileOrString);

  my $package;                                                                  # Package we are in
  my $class;                                                                    # Class we are in
  my $method;                                                                   # Method we are in
  my $state = 0;                                                                # 0 - outer, 1 - parameters to last method

  for(split /\n/, $s)
   {if ($state == 0)
     {if (m(\A\s*package\s+((\w+|\.)+)))                                        # 'package' package ;
       {#say STDERR "Package = $1";
        $package = $1;
       }
      elsif (m(\A\s*public\s*class\s+(\w+)\s*\{?\s*//\s+(.*?)\s*\Z))            # 'public class' Class with optional '{'
       {#say STDERR "Class = $1 = $2";
        $class = $1;
        $javaDoc->classes->{$class} = $parse->{$package}{$class}{comment} = $2; # Record the last encountered class as the class to link to - could be improved!
       }
      elsif (m(\A\s*public\s*(.+?)\s+(\w+)\s*(\(\))?\s*\(?\s*//\s+(.*?)\s*\Z))  # 'public' Method with either '()' meaning no parameters or optional '('
       {#say STDERR "Method = $1 == $2 == $4 ";
        $method = $2;
        $parse->{$package}{$class}{methods}{$method}{type} = $1;
        $parse->{$package}{$class}{methods}{$method}{comment} = $4;
        $state = 1 unless $3;                                                   # Get parameters next unless method does not have any
       }
     }
    elsif ($state == 1)
     {if (m(\A\s*\(?\s*final\s+(.+?)\s*(\w+)\s*[,\)\{]*\s*//\s+(.*?)\s*\Z))     # Optional '(', 'final', type name, optional ',){', comment
       {#say STDERR "Parameter = $1 = $2 = $3";
        push @{$parse->{$package}{$class}{methods}{$method}{parameters}},
             [$javaDoc->urlIfKnown($1), $2, $3];
       }
      else                                                                      # End of parameters if the line does not match
       {$state = 0;
        my $m = $parse->{$package}{$class}{methods}{$method};
        my @p = @{$m ->{parameters}};
        $m->{nameSig} = join ', ', map {$_->[1]} @p;
        $m->{typeSig} = join ', ', map {$_->[0]} @p;
       }
     }
   }
  #say STDERR "AAAA ", dump($parse); exit;
  $parse
 }

sub parseJavaFiles($)                                                           # Parse all the input files into one parse tree
 {my ($javaDoc)  = @_;                                                          # Java doc processor
  for(@{$javaDoc->source})                                                      # Extend the parse tree with the parse of each source file
   {$javaDoc->parseJavaFile($_);
   }
 }

sub htmlJavaFiles($)                                                            # Create documentation using html for all java files from combined parse tree
 {my ($javaDoc)  = @_;                                                          # Java doc processor, combined parse tree
  my $parse = $javaDoc->parse;                                                  # Parse tree
  my @h = <<END;
<h1>Packages</h1>
<table border="1" cellspacing="20">
END
  for my $package(sort keys %$parse)
   {push @h, qq(<tr><td><a href="#$package">$package</a></tr>);
   }
  push @h, qq(</table>);

  for my $package(sort keys %$parse)
   {my %package = %{$parse->{$package}};
    push @h, qq(<a name="$package"/>);
    push @h, qq(<h2>Classes in package: <big>$package</big></h2>);

    push @h, <<END;
<table border="1" cellspacing="20">
<tr><th>Class<th>Description</tr>
END
    for my $class(sort keys %package)
     {my %class = %{$package{$class}};
      my $classComment = $class{comment};
      push @h, <<"END";
<tr><td><a href="#$package.$class">$class</a>
    <td>$classComment
</tr>
END
     }
    push @h, qq(</table>);

    for my $class(sort keys %package)
     {my %class = %{$package{$class}};
      my $classComment = $class{comment};
      push @h, qq(<a name="$package.$class"/>);
      push @h, <<END;
<h3>Methods in class: <big>$class</big>, package: $package</h3>
<table border="1" cellspacing="20">
<tr><th>Method<th>Signature<th>Description</tr>
END
      for my $method(sort keys %{$class{methods}})
       {my %method = %{$class{methods}{$method}};
        my $methodComment = $method{comment};
        my $methodSig     = $method{typeSig};
        push @h, <<END;
<tr><td><a href="#$package.$class.$method">$method</a>
    <td>$methodSig
    <td>$methodComment
</tr>
END
       }
      push @h, qq(</table>);

      for my $method(sort keys %{$class{methods}})
       {my %method = %{$class{methods}{$method}};
        my $type = $method{type};
        my $methodComment = $method{comment};
        push @h, qq(<a name="$package.$class.$method"/>);
        push @h, "<h3><b>$method</b> : $type</h3>";
        push @h, "<p>$methodComment</p>";

        if (my $parameters = $method{parameters})
         {my @parameters = @$parameters;
          push @h, q(<h4>Parameters</h4>);
          push @h, q(<table border="1" cellspacing="20">);
          push @h, q(<tr><th>Name<th>Type<th>Description</tr>);
          for my $parameter(@parameters)
           {my ($type, $name, $comment) = @$parameter;
            push @h, qq(<tr><td>$name<td>$type<td>$comment</tr>);
           }
          push @h, q(</table>);
         }
       }
     }
   }

  @h
 }

sub veryWellKnownClasses {<<'END'}
https://developer.android.com/reference/android/app/Activity.html
https://developer.android.com/reference/android/content/Context.html
https://developer.android.com/reference/android/graphics/BitmapFactory.html
https://developer.android.com/reference/android/graphics/Bitmap.html
https://developer.android.com/reference/android/graphics/Canvas.html
https://developer.android.com/reference/android/graphics/drawable/BitmapDrawable.html
https://developer.android.com/reference/android/graphics/drawable/Drawable.html
https://developer.android.com/reference/android/graphics/Matrix.html
https://developer.android.com/reference/android/graphics/Paint.html
https://developer.android.com/reference/android/graphics/Path.html
https://developer.android.com/reference/android/graphics/PorterDuff.Mode.html
https://developer.android.com/reference/android/graphics/RectF.html
https://developer.android.com/reference/android/media/MediaPlayer.html
https://developer.android.com/reference/android/util/DisplayMetrics.html
https://developer.android.com/reference/java/io/ByteArrayOutputStream.html
https://developer.android.com/reference/java/io/DataInputStream.html
https://developer.android.com/reference/java/io/DataOutputStream.html
https://developer.android.com/reference/java/io/File.html
https://developer.android.com/reference/java/io/FileOutputStream.html
https://developer.android.com/reference/java/lang/String.html
https://developer.android.com/reference/java/lang/Thread.html
https://developer.android.com/reference/java/util/Stack.html
https://developer.android.com/reference/java/util/TreeMap.html
https://developer.android.com/reference/java/util/TreeSet.html
https://developer.android.com/studio/command-line/adb.html
END

#1 Attributes                                                                   # Attributes that can be set or retrieved by assignment

if (1) {                                                                        # Parameters that can be set by the caller
  genLValueArrayMethods(qw(source));                                            # A reference to an array of Java source files that contain documentation as well as java
  genLValueScalarMethods(qw(target));                                           # Name of the file to contain the generated documentation
  genLValueArrayMethods(qw(wellKnownClasses));                                    # Reference to an array of urls that contain the class name of well known Java classes such as: L<TreeMap|/https://developer.android.com/reference/java/util/TreeMap.html> which will be used in place of the class name to make it possible to locate definitions of these other classes.
 }

#1 Methods                                                                      # Methods available

sub new                                                                         # Create a new java doc processor
 {bless {};                                                                     # Java doc processor
 }

sub html($)                                                                     # Create documentation using html as the output format. Write the generated html to the file specified by L<target|/target> if any and returtn the generated html as an array of lines.
 {my ($javaDoc)  = @_;                                                          # Java doc processor

  $javaDoc->parseJavaFiles;                                                     # Parse the input files
  my @h = $javaDoc->htmlJavaFiles;                                              # Write as html

  if (my $file = $javaDoc->target)
   {my $h = @h;
    writeFile($file, join "\n", @h);
    say STDERR "$h lines of documentation written to:\n$file";
   }
  @h                                                                            # Return the generated html
 }

# podDocumentation

=encoding utf-8

=head1 Name

Java::Doc - Extract documentation from Java source code.

=head1 Synopsis

 use Java::Doc;

 my $j = Java::Doc::new;
 $j->source = [qw(/home/phil/z/java/layoutText/LayoutText.java)];
 $j->target =  qq(/home/phil/z/java/documentation.html);
 $j->html;

Each source file is parsed for documentation information which is then
collated into one cross referenced html file.

Documentation is extracted for L<packages|/Packages>, L<classes|/Classes>,
L<methods|/Methods>.

=head2 Packages

Lines matching

  package <packageName> ;

are assumed to define packages.

=head2 Classes

Lines matching:

  public class <className>    // <comment>

  public class <className> {  // <comment>

with any B<{> being ignored, are assumed to define a class with the description
of the class contained in the text of the comment extending to the end of the
line.

=head2 Methods

Methods are specified by lines matching:

  public <type> <methodName> ()  // <comment>

  public <type> <methodName>     // <comment>

  public <type> <methodName> (   // <comment>

with the description of the method contained in the text of the comment
extending to the end of the line.

If '()' is present on the line the method is assumed to have no parameters.
Otherwise the parameter descriptions follow one per line on subsequent lines
that match:

  [(]final <type> <parameterName> [,){] // <comment>

with any leading B<(> or trailing B<)>, B<,>, B<{> being ignored, are assumed
to be parameter definitions for the method with the description of the
parameter held in the comment.

=head2 Example

The following fragment of java code provides an example of documentation held as
comments that can be processed by this module:

 package com.appaapps;

 public class LayoutText   // Layout text on a canvas
  {public static void draw // Draw text to fill a fractional area of the canvas
    (final Canvas canvas){ // Canvas to draw on

=head1 Description

The following sections describe the methods in each functional area of this
module.  For an alphabetic listing of all methods by name see L<Index|/Index>.



=head1 Attributes

Attributes that can be set or retrieved by assignment

=head2 source :lvalue

A reference to an array of Java source files that contain documentation as well as java


=head2 target :lvalue

Name of the file to contain the generated documentation


=head2 wellKnownClasses :lvalue

Reference to an array of urls that contain the class name of well known Java classes such as: L<TreeMap|/https://developer.android.com/reference/java/util/TreeMap.html> which will be used in place of the class name to make it possible to locate definitions of these other classes.


=head1 Methods

Methods available

=head2 new()

Create a new java doc processor


=head2 html($)

Create documentation using html as the output format. Write the generated html to the file specified by L<target|/target> if any and returtn the generated html as an array of lines.

  1  $javaDoc  Java doc processor


=head1 Index


1 L<html|/html>

2 L<new|/new>

3 L<source|/source>

4 L<target|/target>

5 L<wellKnownClasses|/wellKnownClasses>

=head1 Installation

This module is written in 100% Pure Perl and, thus, it is easy to read, use,
modify and install.

Standard L<Module::Build> process for building and installing modules:

  perl Build.PL
  ./Build
  ./Build test
  ./Build install

=head1 Author

L<philiprbrenan@gmail.com|mailto:philiprbrenan@gmail.com>

L<http://www.appaapps.com|http://www.appaapps.com>

=head1 Copyright

Copyright (c) 2016-2017 Philip R Brenan.

This module is free software. It may be used, redistributed and/or modified
under the same terms as Perl itself.

=cut



# Tests and documentation

sub test
 {my $p = __PACKAGE__;
  binmode($_, ":utf8") for *STDOUT, *STDERR;
  return if eval "eof(${p}::DATA)";
  my $s = eval "join('', <${p}::DATA>)";
  $@ and die $@;
  eval $s;
  $@ and die $@;
 }

test unless caller;

1;
# podDocumentation
__DATA__
use Test::More tests => 1;

my $j = Java::Doc::new;
$j->source = [<<END];
/*------------------------------------------------------------------------------
Draw text to fill a fractional area of a canvas, justifying remaining space
Philip R Brenan at gmail dot com, © Appa Apps Ltd Inc on 2017.10.07 at 21:39:18
------------------------------------------------------------------------------*/
package com.appaapps;
import android.graphics.*;

public class LayoutText                                                         // Draw text to fill a fractional area of a canvas, justifying remaining space
 {private static RectF drawArea = new RectF(), textArea = new RectF();          // Preallocated areas
  private static Path  textPath = new Path();                                   // Path of text

  public static void draw                                                       // Draw text to fill a fractional area of a canvas, justifying remaining space
   (final Canvas canvas,                                                        // Canvas to draw on
    final String text,                                                          // Text to draw
    final float x,                                                              // Area to draw text in expressed as fractions of the canvas: left   x
    final float y,                                                              // Area to draw text in expressed as fractions of the canvas: top    y
    final float X,                                                              // Area to draw text in expressed as fractions of the canvas: right  x
    final float Y,                                                              // Area to draw text in expressed as fractions of the canvas: bottom y
    final int justX,                                                            // Justification in x: -1=left,   0=center, +1=right
    final int justY,                                                            // Justification in y: +1=bottom, 0=center, -1=top
    final Paint paintF,                                                         // Character paint for foreground - setTextSize(128) or some other reasonable size that Android is capable of drawing text at in a hardware layer. The text will be scaled independently before it is drawn to get an optimal fit in the drawing area
    final Paint paintB                                                          // Optional character paint for background or null. Both foreground and background text paints must have the same textSize. The foreground is painted last over the background which is painted first.
   )

   {if (paintB != null && paintF.getTextSize() != paintB.getTextSize())         // Check  the text size in each paint matches
     {say("Different text sizes in supplied at 21:39:18paints, using foreground ",
          " size in both paints");
      paintB.setTextSize(paintF.getTextSize());                                 // Equalize the paint text sizes if different
     }

    if (true)                                                                   // Draw area dimensions
     {final float w = canvas.getWidth(), h = canvas.getHeight();                // Canvas dimensions
      drawArea.set(x*w, y*h, X*w, Y*h);                                         // Draw area
     }

    paintF.getTextPath(text, 0, text.length(), 0, 0, textPath);                 // Layout text with foreground paint
    textPath.computeBounds(textArea, true);                                     // Text bounds

    final float
      Aw = textArea.width(), Ah = textArea.height(),                            // Dimensions of text
      aw = drawArea.width(), ah = drawArea.height(),                            // Dimensions of draw area
      A  = Aw * Ah,                                                             // Area of the incoming text
      a  = aw * ah,                                                             // Area of the screen to be filled with the text
      S  = (float)Math.sqrt(a / A),                                             // The scaled factor to apply to the  text
      n  = max((float)Math.floor(ah / S / Ah), 1f),                             // The number of lines - at least one
      s  = min(ah / n / Ah, aw * n / Aw),                                       // Scale now we know the number of lines
      d  = Aw / n,                                                              // The movement along the text for each line
      dx = n == 1 ? aw - Aw * s : 0,                                            // Maximum x adjust - which only occurs if we have just one line
      dy = ah - Ah * s * n;                                                     // Maximum y adjust

//    say("AAAA ", " Aw=", Aw, " Ah=", Ah,                                      // Debug scaling
//                 " aw=", aw, " ah=", ah,
//                 " A=",  A , " a=",  a, " S=", S, " n=",  n, " s=", s,
//                 " d=", d,   " dx=", dx, " dy=", dy, " ta="+textArea);
//
    canvas.save();
    canvas.clipRect(drawArea);                                                  // Clip drawing area
    canvas.translate                                                            // Move to top left corner of drawing area
     (drawArea.left + (justX < 0 ? 0 : justX > 0 ? dx : dx / 2f)                // Distribute remaining space in x
                    - textArea.left * s,                                        // Offset in x to start of first character
      drawArea.top  + (justY < 0 ? 0 : justY > 0 ? dy : dy / 2f)                // Distribute the remaining space in y
                    - textArea.bottom * s                                       // Offset in y to start of first character
     );
    canvas.scale(s, s);                                                         // Scale
    canvas.translate(0, Ah);                                                    // Down one line

    for(int i = 0; i < n; ++i)                                                  // Draw each line
     {if (paintB != null) canvas.drawText(text, 0, 0, paintB);                  // Text background
      canvas.drawText(text, 0, 0, paintF);                                      // Text foreground
      canvas.translate(-d, Ah);                                                 // Down one line and back
     }

    canvas.restore();
   }

  private static void say(Object...O) {final StringBuilder b = new StringBuilder(); for(Object o: O) b.append(o.toString()); System.err.print(b.toString()+"\n");}
  private static float max(float a, float b) {if (a > b) return a; return b;}
  private static float min(float a, float b) {if (a < b) return a; return b;}
 }
END

my @h = $j->html;

ok 29 == @h;
