#!/usr/bin/env perl
$|++;
use lib q|/Users/johanl/Documents/Projects/Dev/CPAN2/Devel-PerlySense/source/lib|;
use lib "/Users/johanl/Documents/Projects/Dev/CPAN2/Devel-CoverX-Covered/trunk/source/lib";
use lib "/home/j.lindstrom/dev/personal/p5-Devel-PerlySense/source/lib";

use strict;

# Must do this early to silence noisy CPAN modules which keep breaking
# the output
use Devel::PerlySense::Util::Log;
BEGIN { $SIG{__WARN__} = sub { debug(@_); }; }

use Getopt::Long;
use Pod::Usage;
use File::Basename;
use File::Path;
use File::Find;
use Path::Class;
use Cache::FileCache;
use List::Util qw/ max /;
use List::MoreUtils qw/ any /;

use lib "../lib", "lib";
use Devel::PerlySense;
use Devel::PerlySense::Home;
use Devel::PerlySense::Editor;
use Devel::PerlySense::Editor::Emacs;
use Devel::PerlySense::Editor::Vim;
use Time::HiRes qw/time/;
use YAML::Tiny;
use Cwd;

use Data::Dumper;
use Devel::TimeThis;


main();

sub main {

    my $oPs = createPerlySense();

    if (($ARGV[0] || "") eq "--stdin") {

        chomp(my $dir = <STDIN>);
        chomp(my $command = <STDIN>);
        $dir && $command or warn(q|Please enter ABS_DIR\nARGV\n| . "\n"), exit(0);

        chdir($dir) or warn("Could not chdir to ($dir)\n"), exit(0);

        local @ARGV = split(/\s+/, $command);
        s/^(["'])(.*)\1$/$2/ for @ARGV; #Remove quotes

        eval { main_perly_sense($oPs) };
        $@ and print $@;
    } else {
#     my $total = Devel::TimeThis->new("Total loop");
#      for (1..5) {
#          my $iteration = Devel::TimeThis->new("Iteration");
#          local @ARGV = @ARGV;
#          sleep(5);
        main_perly_sense($oPs);
#      }
    }
}





#Oh yes, this file is in serious need of a makeover...
sub main_perly_sense {
    my ($oPs) = @_;

    my ($dirOrigin, $fileOrigin, $module, $dir, $file, $row, $col, $sub, $nameClass, $nameMethod, $clearCache, $widthDisplay, $typeIo, $useAlternateCommand);
    my @aShow;
    debug("CWD: (" . getcwd() . "), ARGV: (" . join(" ", map { "|$_|" } ("perly_sense",  @ARGV)) . ")");

    GetOptions(
        "module:s"              => \$module,
        "file:s"                => \$file,
        "dir:s"                 => \$dir,
        "row:i"                 => \$row,
        "col:i"                 => \$col,
        "sub:s"                 => \$sub,
        "file_origin:s"         => \$fileOrigin,
        "dir_origin:s"          => \$dirOrigin,
        "class_name:s"          => \$nameClass,
        "method_name:s"         => \$nameMethod,
        "show:s"                => \@aShow,
        "clear_cache"           => \$clearCache,
        "use_alternate_command" => \$useAlternateCommand,
        "width_display:s"       => \$widthDisplay,
        "io_type:s"             => \$typeIo,
    );
    my ($command) = @ARGV;
    $command or syntax("Missing command");

    $typeIo ||= "editor_emacs";
    my $rhIoClass = {
        editor_emacs => "Devel::PerlySense::Editor::Emacs",
        editor_vim   => "Devel::PerlySense::Editor::Vim",
    };
    my $classIo = $rhIoClass->{$typeIo}
            or syntax("Invalid --io_type ($typeIo)");
    my $oEditor = $classIo->new(
        oPerlySense => $oPs,
        widthDisplay => $widthDisplay,
    );

    if ($clearCache && (my $dirHomeCache = $oPs->oHome->dirHomeCache)) {
        warn("Clearing cache directory ($dirHomeCache)\n");
        rmtree([$dirHomeCache]); -d $dirHomeCache and die("Could not clear cache ($dirHomeCache)\n");
        mkpath([$dirHomeCache]); -d $dirHomeCache or die("Could not re-create cache dir ($dirHomeCache)\n");
    }





    if($command eq "external_dir") {
        print Devel::PerlySense::Editor->dirExtenal;
    } elsif($command eq "find_module_source_file") {
        $fileOrigin and $dirOrigin = dirname($fileOrigin);
        $dirOrigin ||= ".";
        $module or syntax("Missing option --module");

        eval {
            my $file = $oPs->fileFindModule(nameModule => $module, dirOrigin => $dirOrigin) || "";
            print $file;
        };
        $@ and warn("Internal error: $@\n");

    } elsif($command eq "find_callers") {
        $fileOrigin and $dirOrigin = dirname($fileOrigin);
        $dirOrigin ||= ".";
        $sub or syntax("Missing option --sub");

        eval {
            my $callSites = $oPs->raCallSiteForMethod(
                nameMethod => $sub,
                dirOrigin  => $dirOrigin,
            );
            print $oEditor->formatOutputDataStructure(
                rhData => { callers => $callSites },
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "visualize_callers") {
        my $source = "";
        # Wow, this is getty waaaay too hacky. Replace with a web server already!.
        while(my $line = <STDIN>) {
            chomp($line);
            $line eq "" and last;
            $source .= "$line\n";
        }

        eval {
            my $rhFileCaller = $oPs->rhFileCallerVisualized(
                source => $source,
            );
            print $oEditor->formatOutputDataStructure(
                rhData => $rhFileCaller,
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "display_module_pod") {
        $fileOrigin and $dirOrigin = dirname($fileOrigin);
        $dirOrigin ||= ".";
        $module or syntax("Missing option --module");

        eval {
            if(my $file = $oPs->fileFindModule(nameModule => $module, dirOrigin => $dirOrigin)) {
                my $pod = $oPs->podFromFile(file => $file) || "";
                print $oEditor->formatOutputDataStructure(rhData => {pod => $pod});
            } else {
                print $oEditor->formatOutputDataStructure(
                    rhData => {message => "Module ($module) not found"},
                );
            }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "smart_go_to") {
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            if(my $oLocation = $oPs->oLocationSmartGoTo(
                file => $file,
                row => $row,
                col => $col,
            )) {
                print $oEditor->formatOutputDataStructure(
                    rhData => {
                        file => $oLocation->file,
                        row  => $oLocation->row,
                        col  => $oLocation->col,
                    },
                );
            }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "smart_doc") {
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            if(my $oLocation = $oPs->oLocationSmartDoc(
                file => $file,
                row => $row,
                col => $col,
            )) {
                print $oEditor->formatOutputDataStructure(
                    rhData => {
                        found    => $oLocation->rhProperty->{found},
                        name     => $oLocation->rhProperty->{name},
                        doc_type => $oLocation->rhProperty->{docType},
                        text     => $oLocation->rhProperty->{text} || "",
                    },
                );
           }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "class_overview") {
        $file || $nameClass or syntax("Missing option --file or --class_name");

        eval {

            my $oClass;
            if($file) {
                $row or syntax("Missing option --row");
                $col or syntax("Missing option --col");

                $oClass = $oClass = $oPs->classAt(
                    file => $file,
                    row => $row,
                    col => $col,
                );
            }
            else {
                $dirOrigin or syntax("Missing option --dir_origin");

                $oClass = $oClass = $oPs->classByName(
                    name => $nameClass,
                    dirOrigin => $dirOrigin,
                );
            }

            if($oClass) {
                my $raClassOverviewShow = $oEditor->raClassOverviewShow;

                my $raOverviewShow = $oEditor->raClassOverviewShowDefault;
                if(@aShow) {
                    $raOverviewShow = [ @aShow ];
                    for my $show (@$raOverviewShow) {
                        if( ! any(sub { $show eq $_ }, @$raClassOverviewShow) ) {
                            my $showValuesValid = join(", ", @$raClassOverviewShow);
                            die("Invalid --show option ($show). Valid values are: $showValuesValid\n");
                        }
                    }
                }

                my $overview = $oEditor->formatOutputDataStructure(
                    rhData => {
                        class_name => $oClass->name,
                        class_overview => $oEditor->classOverview(
                            oClass => $oClass,
                            raShow => $raOverviewShow,
                        ),
                        message => "Class Overview for (" . $oClass->name . ")",
                        dir => $oClass->dirModule,
                    },
                );
                debug($overview);
                print $overview;
            } else {
                print $oEditor->formatOutputDataStructure(
                    rhData => {
                        message => "No class found",
                    },
                );
            }
        };
        $@ and warn("Error: $@\n");

    } elsif($command eq "method_doc") {
        $nameClass or syntax("Missing option --class_name");
        $nameMethod or syntax("Missing option --method_name");
        $dirOrigin or syntax("Missing option --dir_origin");

        eval {
            my $oClass = $oPs->classByName(
                name => $nameClass,
                dirOrigin => $dirOrigin,
            );
            if($oClass) {
                if (my $oLocation = $oClass->oLocationMethodDoc(method => $nameMethod)) {
                    print $oEditor->formatOutputDataStructure(
                        rhData => {
                            found    => $oLocation->rhProperty->{found},
                            name     => $oLocation->rhProperty->{name},
                            doc_type => $oLocation->rhProperty->{docType},
                            text     => $oLocation->rhProperty->{text} || "",
                        },
                    );
                }
            }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "inheritance_doc") {
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            my $oClass = $oPs->classAt(file => $file, row => $row, col => $col);
            if($oClass) {
                my $textInheritance = $oEditor->textClassInheritance(oClass => $oClass);
                chomp($textInheritance);
                print $oEditor->formatOutputDataStructure(
                    rhData => {
                        class_name =>  $oClass->name,
                        class_inheritance => $textInheritance,
                    },
                );
            }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "use_doc") {
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            my $oClass = $oPs->classAt(file => $file, row => $row, col => $col);
            if($oClass) {
                my $textUses = $oEditor->textClassUses(oClass => $oClass);
                chomp($textUses);
                print $oEditor->formatOutputDataStructure(
                    rhData => {
                        class_name =>  $oClass->name,
                        class_use => $textUses,
                    },
                );
            }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    ###TODO: unify the output to use the formatOutputDataStructure,
    ###this is just silly and doing way too much
    } elsif($command eq "method_go_to") {
        if($nameClass) {
            $dirOrigin or syntax("Missing option --dir_origin");
        }
        elsif ($file) {
            $row && $col or syntax("Missing option --row, and --col");
        }
        else {
            syntax("Missing option --class_name or --file");
        }

        eval {
            my $oClass;
            if($nameClass) {
                $oClass = $oPs->classByName(
                    name => $nameClass,
                    dirOrigin => $dirOrigin,
                );
            }
            else {
                $oClass = $oPs->classAt(file => $file, row => $row, col => $col);
            }

            if($oClass) {
                my $oLocation = $oClass->oLocationMethodGoTo(method => $nameMethod);

                if($nameClass) {
                    if ($oLocation) {
                        print join("\t", $oLocation->file, $oLocation->row, $oLocation->col);
                    }
                }
                else {
                    print $oEditor->formatOutputDataStructure(
                        rhData => {
                            file =>  $oLocation->file,
                            row => $oLocation->row,
                            col => $oLocation->col,
                        },
                    );
                }
            };
            $@ and warn("Error: $@\n");
        }

    } elsif($command eq "base_class_go_to") {
        ###TODO: Too much code here, push into method in Class or
        ###PerlySense, tests
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            my $oClass = $oPs->classAt(file => $file, row => $row, col => $col);
            if($oClass) {
                my $oLocationSub = $oClass->oLocationSubAt(row => $row, col => $col);
                my $nameSub = "";
                $oLocationSub and $nameSub = $oLocationSub->rhProperty->{nameSub};
                my $raClassInfo = [
                    map {
                        my $oClassBase = $_;

                        my $nameBaseSub = "";
                        my $rowBaseSub = 0;
                        my $description = $oClassBase->name;
                        if($nameSub) {
                            if( my $oLocationBaseSub = $oClassBase->oLocationSub(name => $nameSub) ) {
                                $nameBaseSub = $oLocationBaseSub->rhProperty->{nameSub};
                                $rowBaseSub = $oLocationBaseSub->row;
                                $description .= "->$nameBaseSub";
                            }
                        }

                        chomp( my $class_inheritance = $oEditor->textClassInheritance(
                            oClass => $oClassBase,
                        ));
                        {
                            class_name => $oClassBase->name,
                            class_description => $description,
                            class_inheritance => $class_inheritance,
                            file => $oClassBase->raDocument->[0]->file,
                            row => $rowBaseSub,
                        }
                    } values %{$oClass->rhClassBase()}
                ];
                print $oEditor->formatOutputDataStructure(
                    rhData => { class_list => $raClassInfo },
                );
            }
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "test_other_files") {
        $file or syntax("Missing option --file");

        eval {
            $oPs->setFindProject(file => $file);
            my $raFileTestOther = $oPs->raFileTestOther(file => $file, sub => $sub);
            @$raFileTestOther or die("No related test files found\n");

            print $oEditor->formatOutputDataStructure(
                rhData => {
                    other_files => $raFileTestOther,
                    project_dir => $oPs->oProject->dirProject,
                },
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "project_other_files") {
        $file or syntax("Missing option --file");

        eval {
            $oPs->setFindProject(file => $file);
            my $raFileProjectOther = $oPs->raFileProjectOther(file => $file, sub => $sub);
            @$raFileProjectOther or die("No related project files found\n");

            print $oEditor->formatOutputDataStructure(
                rhData => {
                    other_files => $raFileProjectOther,
                    project_dir => $oPs->oProject->dirProject,
                },
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "run_file") {
        $file or syntax("Missing option --file");
        my $commandKey = "command";
        $useAlternateCommand and $commandKey = "alternate_command";

        eval {
            print $oEditor->formatOutputDataStructure(
                rhData => $oPs->rhRunFile(file => $file, keyConfigCommand => $commandKey),
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "debug_file") {
        $file or syntax("Missing option --file");
        my $commandKey = "command";
        $useAlternateCommand and $commandKey = "alternate_command";

        eval {
            print $oEditor->formatOutputDataStructure(
                rhData => $oPs->rhDebugFile(file => $file, keyConfigCommand => $commandKey),
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "flymake_file") {
        $file or syntax("Missing option --file");

        eval { $oPs->flymakeFile(file => $file) };
        if(my $err = $@) {
            chomp($err);
            debug($err);
            print $err;
        }

    } elsif($command eq "create_project") {
        $dir ||= ".";

        $oPs->createProject(dir => $dir);
        print "PerlySense Project Created\n";

    } elsif($command eq "class_api") {
        # Experimental
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            my ($package, $oApi) = $oPs->aApiOfClass(file => $file, row => $row, col => $col);
            if($oApi) {
                my $rhSub = $oApi->rhSub;
                print "$package\n" . join("\n",
                           map {
                               sprintf("%s:%d %s", $_->{file}, $_->{row}, $_->rhProperty->{sub});
                           } sort {
                               $a->{file} cmp $b->{file}
                                       or
                               $a->{row} <=> $b->{row}
                           }
                           values %$rhSub
                       );
            }
        };
        $@ and warn("Error: $@\n");

    } elsif($command eq "process_inc") {
        processFilesInDirs($oPs, \@INC);

    } elsif($command eq "process_project") {
        $oPs->setFindProject(dir => $dir || ".");
        processFilesInDirs($oPs, [ $oPs->oProject->dirProject ]);

    } elsif($command eq "regex") {
        $file or syntax("Missing option --file");
        $row or syntax("Missing option --row");
        $col or syntax("Missing option --col");

        eval {
            print $oEditor->formatOutputDataStructure(
                %{ $oPs->rhRegexExample(file => $file, row => $row, col => $col) },
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "covered_subs") {
        $file or syntax("Missing option --file");

        eval {
            print $oEditor->formatOutputDataStructure(
                rhData => {
                    sub_quality => $oPs->rhSubCovered(file => $file),
                },
            );
        };
        if(my $err = $@) {
            chomp($err);
            print $oEditor->formatOutputDataStructure( rhData => { message => $err } );
        }

    } elsif($command eq "info") {
        $dir ||= ".";
        $oPs->setFindProject(dir => $dir);

#        print "PROJECT CONFIG: " . YAML::Tiny::Dump($oPs->rhConfig);
        print "VERSION: (" . Devel::PerlySense->VERSION . ")\n";
        print "PROJECT: (" . $oPs->oProject->dirProject . ")\n";
        print "HOME: (" . $oPs->oHome->dirHome . ")\n";
        print "HOME DEBUG LOG: (" . file($oPs->oHome->dirHome, "log", "debug.log") . ")\n";

    } elsif($command eq "log_file") {
        $dir ||= ".";
        $oPs->setFindProject(dir => $dir);

        ###TODO: extract property of Util::Log?
        print file($oPs->oHome->dirHome, "log", "debug.log") . "\n";

    } elsif($command eq "project_dir") {
        $dir ||= ".";
        $oPs->setFindProject(dir => $dir, file => $file);
        print $oEditor->formatOutputDataStructure(
            rhData => { project_dir => $oPs->oProject->dirProject },
        );

    } elsif($command eq "vcs_dir") {
        $dir ||= ".";
        $oPs->setFindProject(dir => $dir);
        print $oEditor->formatOutputDataStructure(
            rhData => {
                project_dir => $oPs->oProject->dirProject,
                vcs_name    => $oPs->oProject->nameVcs,
            },
        );

    } else {
        syntax("Invalid command ($command)");
    }
    return(1);
}



sub syntax {
    my ($message) = @_;
    print "Error: $message\n\n";    #pod2usage -msg doesn't do this for me... :(
    pod2usage(-verbose => 2, exitval => "NOEXIT");
    die("\n");
}



###TODO: refactor: make method in PerlySense
sub processFilesInDirs {
    my ($oPs, $raDir) = @_;

    print file($oPs->oHome->dirHome, "log", "debug.log") . "\n";

    my @aFile;
    find({
        wanted => sub { /\.pm$/ and push(@aFile, file($_)->absolute . ""); },
        no_chdir => 1,
    }, @$raDir);

    my %hFileTime;
    for my $file (@aFile) {
        debug("Processing file ($file)");
        my $timeStart = time();
        eval {
            $oPs->oDocumentParseFile($file);
            $oPs->clearInMemoryDocumentCache();
        };
        $@ and debug($@);
        my $timeDuration = time() - $timeStart;
        $hFileTime{$file} = $timeDuration;
        debug(sprintf("  %0.6fs", $timeDuration));
    }

    debug("REPORT");
    for my $file (
        sort { $hFileTime{$a} <=> $hFileTime{$b} }
                keys %hFileTime) {
        debug(sprintf("%0.6f : $file", $hFileTime{$file}));
    }

}



sub createPerlySense {

    my $oPs = Devel::PerlySense->new();

    my $oHome = $oPs->oHome;
    my $dirHomeCache = $oHome->dirHomeCache
            or warn(
                "Could not create cache dir in either of:\n",
                join(":", $oHome->aDirHomeCandidate) . "\n",
            );
    my $oCache = Cache::FileCache->new({cache_root => $dirHomeCache})
            or warn("Could not create cache in ($dirHomeCache)\n");
    $oPs->oCache($oCache);

    return $oPs;
}



__END__

=head1 NAME

perly_sense -- CLI for L<Devel::PerlySense> with support for Emacs and
Vim output formats.


=head1 OPTIONS

perly_sense COMMAND OPTIONS

(Note that these docs are incomplete. if you really need to know, read
the source of the script).

=head2 smart_go_to --file, --row, --col

Look in --file at --row, --col and print a location for the origin of
the source at that location,


=head2 smart_doc --file, --row, --col

Look in --file at --row, --col and print a smart doc text for the
source at that location


=head2 class_overview --file, --row, --col

Look in --file at --row, --col and find the class overview for the
package at that location. Return keys:

If a class was found: class_overview, class_name, message.

If a class was not found: message.

On error: error.


=head2 process_inc

Process and cache all modules found in @INC


=head2 process_project

Process and cache all modules found in the current project


=head2 --clear_cache

Clears the cache before doing anything else

=head2 --width_display=COLUMNS

Sets the display width COLUMNS wide. Default can be configured per
Project
.


=head1 SYNOPSIS

perly_sense smart_doc --file=perly_sense --row=102 --col=42

perly_sense smart_go_to --file=Foo/Bar.pm --row=32 --col=3

perly_sense smart_doc --file=Foo/Bar.pm --row=32 --col=3

perly_sense process_inc



=head1 EDITOR INTEGRATION

If you want to use this from within an editor/IDE you need to be able to shell
out to call this program and to do something useful with the result using the
editor's macro/programming facilities.

=cut
