#| Utilities for interacting with idris and associated tooling
unit module IUtils;

need IUtils::IDEMode;

use Terminal::ANSIColor;

use IUtils::Regexes;
use IUtils::Compiler;

use paths;

#| Structure representing a test
class Test {
    #| The name of the test
    has Str:D $.name is required;
    #| The expression name of the test
    has Str:D $.expr is required;
    #| The output type of the test
    has ExprOutput:D $.output-type is required;

    #| Run this test, and return true if it failed, false if it passed
    method run(IO::Path:D $source, Int:D $indent-level? = 4 --> Bool) {
        CATCH {
            when ExpressionError {
                say "{colored '+', 'red'} $.name: {colored 'FAIL', 'red bold'}"
                    .indent($indent-level);
                say "{colored('exit code', 'red')}: {$_.exit-code}"
                    .indent($indent-level + 2);
                if $_.out.trim {
                    say colored('stdout:', 'underline')
                        .indent($indent-level + 2);
                    say $_.out.trim.lines.map(*.indent($indent-level + 2))
                        .join("\n");
                }
                # say $_.err.elems;
                # if $_.err.trim {
                #     say $_.err.trim.lines.elems;
                #     say colored('stderr:', 'underline')
                #         .indent($indent-level + 2);
                #     say $_.err.trim.lines.map(*.indent($indent-level + 2))
                #         .join("\n");
                # }
                return True;
            }
        }

        idris-exec $.expr, $source.relative, $.output-type;
        # The exception handler graps flow if the test failed, here the test passed
        my $output =
            "{colored '+', 'green'} $.name: {colored 'pass', 'green'}";
        say $output.indent($indent-level);
        return False;
    }
}

#| Structure representing the tests in a module
class ModuleTests {
    #| The name of this module
    has Str:D $.name is required;
    #| The source file of this module
    has IO::Path:D $.source is required;
    #| A list of the associated tests this module has
    has Test:D @.tests is required;
}

#| Structure representing all of the runables assocated with a project
class PackageRunables {
    #| The ipkg for this project
    has IO::Path:D $.ipkg is required;
    # TODO: Add benchmarks
    #| A map from the name of the module to a list of tests
    has ModuleTests:D %.tests is required;

    #| Check to see if this package contains a given module
    method contains-module(Str $module-name --> Bool) {
       %!tests{$module-name}:exists
    }
}

#| Structure representing the root of what idris considers a package directory,
#| with the associated ipkg and source files. These can and will overlap within
#| the same directory.
class PackageInfo {
    has IO::Path:D $.ipkg is required;
    has IO::Path:D $.root is required;
    has IO::Path:D @.sources is required;

    method runnables {
        # Locate the tests
        my %tests = Hash.new;
        for @.sources -> $source {
            my $contents = $source.slurp;
            if $contents ~~ &module-name {
                my $module-name = $<name>.Str;
                my @tests;
                for $contents.match(&flagged-expression, :g) -> $match {
                    my $output-type = do
                        given $match<output-type> {
                            when * eq '()' {succeed Unit};
                            when * eq 'Bool' {succeed Boolean};
                            when * eq 'Either' {succeed Either};
                        };
                    my $test =
                        Test.new(name => $match<test-name>.Str.trim,
                                 expr => $match<expression-name>.Str,
                                 output-type => $output-type);
                    @tests.push($test);
                }
                if @tests.elems > 0 {
                    %tests{$module-name} =
                        ModuleTests.new(name => $module-name,
                                        source => $source,
                                        tests => @tests);
                }
            }
        }
        # Build and return the runnables
        PackageRunables.new(ipkg => self.ipkg, tests => %tests)
    }
}

#| Scan a particular ipkg for its associated sources
sub scan-ipkg(IO::Path:D $ipkg --> PackageInfo:D) {
    my $contents = $ipkg.slurp;
    my $src-dir =
        ($contents ~~
         / 'sourcedir' \h* '=' \h*
           '"' $<value>=[<-["]>*] '"' /)<value>
            // "src";

    my sub is-source(Str:D $file --> Bool) {
       return $file.ends-with(".idr") || $file.ends-with(".md");
    }

    my IO::Path:D @sources =
        paths($ipkg.parent.add($src-dir), :file(&is-source)).map(*.IO);
    PackageInfo.new(ipkg => $ipkg, root => $ipkg.parent, sources => @sources)
}

# TODO: Add some parsing of pack.toml to locate test packages and associate them
# with their source ipkg

#| Scan $*CWD to locate ipkgs and their associated sources
sub scan-packages(--> Array[PackageInfo:D]) is export {
    my PackageInfo:D @ipkgs =
        paths(:file(*.ends-with(".ipkg"))).map(*.IO.&scan-ipkg);
    # Check out the parents if we didn't find an ipkg
    if @ipkgs {
        return @ipkgs;
    } else {
        indir $*CWD.parent, {scan-packages};
    }
}