The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package UR::Namespace::Command::Show::Subclasses;

use strict;
use warnings;
use UR;
use YAML;

my $spacing = '';

class UR::Namespace::Command::Show::Subclasses {
    is => 'Command::V2',
    has=> [
        superclass => {
            is => 'Text',
            shell_args_position => 1,
            doc => 'Only show subclasses of this class.',
        },
        color => {
            is => 'Boolean',
            is_optional => 1,
            default_value => 1,
            doc => 'Display in color.',
        },
        maximum_depth => {
            is => 'Int',
            is_optional => 1,
            default_value => -1,
            doc => 'Maximum subclass depth.  Negative means infinite.',
        },
        recalculate => {
            is => 'Boolean',
            is_optional => 1,
            default_value => 0,
            doc => 'Recreate the cache instead of using the results of a previous run.',
        },
        flat => {
            is => 'Boolean',
            is_optional => 1,
            doc => 'Simply prints the subclass names with no other formatting or coloring.',
        }
    ],
    doc => 'Display subclasses of a given class.',
};

sub help_synopsis {
    my $self = shift;
    my $result .= <<EOP;
    Displays a tree of subclasses of a given class.
EOP
    return $result;
}

sub help_detail {
    my $self = shift;
    my $result .= <<EOP;
Displays a tree containing the names (and optionally other info) of the subclasses
of a given class.

    ur show subclasses <class_name>
    ur show subclasses <class_filename>

EOP
    return $result;
}

sub _mine_tree_for_class {
    my ($tree, $name, $result) = @_;

    if(ref($tree) eq 'HASH') {
        for my $key (keys %{$tree}) {
            if($key eq $name) {
                push(@{$result}, 1);
            } else {
                _mine_tree_for_class($tree->{$key}, $name, $result);
            }
        }
    } elsif(ref($tree) eq 'ARRAY') {
        for my $item (@{$tree}) {
            if($item eq $name) {
                push(@{$result}, 1);
            }
            _mine_tree_for_class($item, $name, $result);
        }
    }
    return;
}

sub execute {
    my ($self) = @_;
    my $indexfile = '/tmp/.ur_class_index';

    my $subclass_index_ref;
    if($self->recalculate or (not -e $indexfile)) {
        my $test_use_cmd = UR::Namespace::Command::Test::Use->create();
        $test_use_cmd->execute();

        $subclass_index_ref = {};
        create_subclass_index('UR::Object', $subclass_index_ref);

        my %subclass_index = %{$subclass_index_ref};
        open(my $output_fh, '>', $indexfile);
        for my $key (keys %subclass_index) {
            print $output_fh sprintf("%s %s\n", $key,
                join("\t", @{$subclass_index{$key}}));
        }
        close($output_fh);
    } else {
        $subclass_index_ref = parse_subclass_index_file($indexfile);
    }

    # check to see if superclass is even in the subclass_index
    my @result;
    _mine_tree_for_class($subclass_index_ref, $self->superclass, \@result);
    unless(@result) {
        my $class_name = $self->color ?
                Term::ANSIColor::colored($self->superclass, 'red') :
                $self->superclass;
        printf "%s is not a valid class, check your spelling or " .
                "see --help (recalculate).\n", $class_name;
        return;
    }

    if($self->flat) {
        $self->display_subclasses_flat($subclass_index_ref, 
                $self->superclass, 0)
    } else {
        $self->display_subclasses($subclass_index_ref,
                $self->superclass, '', '  ', 0);
    }

    return 1;
}

sub create_subclass_index {
    my ($seed, $index_ref) = @_;

    my @children = $seed->__meta__->subclasses_loaded;
    for my $child (@children) {
        my @parents = @{$child->__meta__->{is}};
        for my $parent (@parents) {
            if($index_ref->{$parent}) {
                push(@{$index_ref->{$parent}}, $child);
            } else {
                $index_ref->{$parent} = [$child];
            }
        }
    }
}

sub parse_subclass_index_file {
    my ($indexfile) = @_;

    open(IN, '<', $indexfile);
    my %index;
    while(my $line = <IN>) {
        chomp($line);
        if($line) {
            my ($parent, $rest) = split(/ /, $line);
            if($rest) {
                my @children = split('\t', $rest);
                $index{$parent} = \@children;
            } else {
                $index{$parent} = [];
            }
        }
    }
    return \%index
}

sub display_subclasses_flat {
    my ($self, $index_ref, $name, $depth) = @_;
    my $maximum_depth = $self->maximum_depth;
    if($depth == $maximum_depth + 1 and $maximum_depth != -1) {
        return;
    }
    print "$name\n";

    # get the children
    my $children_ref = $index_ref->{$name};
    my @children;
    if($children_ref) {
        @children = @{$index_ref->{$name}};
    } else { # if it isn't in index it has no children.
        @children = ();
    }

    # loop over children
    for my $child (@children) {
        $self->display_subclasses_flat($index_ref, $child, $depth+1);
    }
}

sub display_subclasses {
    my ($self, $index_ref, $name, $global_prefix, $personal_prefix, $depth) = @_;
    my $maximum_depth = $self->maximum_depth;

    my ($dgp, $dpp, $dn) = ($global_prefix, $personal_prefix, $name);
    if($self->color) {
        ($dgp, $dpp, $dn) = colorize_output($global_prefix, $personal_prefix,
                $name, $self->superclass);
    }
    print join('', $dgp, $dpp, $spacing, $dn);

    my $o = ($personal_prefix =~ /^\|/ ) ? '|' : ' ';
    my $child_global_prefix = sprintf("%s%s  %s", $global_prefix, $o, $spacing);

    # get the children
    my $children_ref = $index_ref->{$name};
    my @children;
    if($children_ref) {
        @children = @{$index_ref->{$name}};
    } else { # if it isn't in index it has no children.
        @children = ();
    }

    # loop over children
    my $len_children = scalar(@children);
    if($len_children and $depth == $maximum_depth and $maximum_depth != -1) {
        print " ...\n";
        return;
    }
    print "\n";

    my $i = 1;
    for my $child (@children) {
        my $child_personal_prefix = ($len_children == $i) ? '`-' : '|-';

        $self->display_subclasses($index_ref, $child, $child_global_prefix,
                $child_personal_prefix, $depth+1);
        $i += 1;
    }
}

sub colorize_output {
    my ($global_prefix, $personal_prefix, $name, $superclass) = @_;

    my $dgp = Term::ANSIColor::colored($global_prefix, 'white');
    my $dpp = Term::ANSIColor::colored($personal_prefix, 'white');
    my $name_prefix = $name;
    if($name_prefix =~ /^($superclass)/) {
        $name_prefix = $superclass;
    } else {
        $name_prefix = '';
    }
    my $name_suffix = $name;
    $name_suffix =~ s/^($superclass)//;
    my $dn = sprintf("%s%s", Term::ANSIColor::colored($name_prefix, 'white'), $name_suffix );

    return ($dgp, $dpp, $dn);
}

1;