use QM;
use Utils;
use Terminal::Table::Style;
use Terminal::Table::Generator;

#|«
Print the group tables, the prime implicants chart and the 
reduced boolean expression for the given minterms.
»
multi MAIN( 
    'all',
    Str :$m!,    #= Space-separated list of minterms (e.g., -m='1 4 6 8')
    Str :$d = '' #= Space-separated list of don't care conditions
) {
    MAIN('groups',  :$m, :$d);
    MAIN('chart',   :$m, :$d);
    MAIN('formula', :$m, :$d);
}

#| Print the group tables.
multi MAIN( 'groups', Str :m($minterms) is required, Str :d($dont-care) = '' ) {
    my @m = $minterms.comb(/\d+/);
    my @d = $dont-care.comb(/\d+/);

    my $qm = QM.minterms(@m, @d);
    
    # group minterms by # of 1 bits required to represent them
    my @all = (@m, @d).flat».Int.sort;
    my $bits-num = @all.tail.base(2).chars;
    my %groups = @all.classify(
        { $_.base(2).comb('1').Int },
        :as{"%0*d".sprintf($bits-num, $_.base(2))}
    );

    # print the corresponding table for the first group
    print-table-for-groups(%groups);

    loop {
        my Int $table-num    = 1;
        my SetHash $marked  .= new;
        my Bool $should-stop = True;

        my %tmp-groups = %groups;
        %groups        = %();
        my @idx        = %tmp-groups.keys.sort;

        for @idx[0..^*] -> $i {
            for %tmp-groups{ @idx[$i] }.flat X %tmp-groups{ @idx[$i+1] }.flat {
                my ($j, $k) = ($_.first, $_.tail);
                my @res = compare($j, $k);
                if @res.head {
                    my $minterm = $j.substr(0..^@res.tail) ~ '-' ~ $j.substr(@res.tail^..*);
                    if $minterm ∉ %groups{$i} {
                        %groups{$i}.push: $minterm;
                    }
                    $should-stop = False;
                    $marked{$j}++;
                    $marked{$k}++;

                }
            }
            $table-num += 1;
        }

        # print the unchecked minterms for this table
        put 'Unchecked elements (Prime Implicants) of this table: ',
            $qm.prime-implicants{ 'table' ~ $table-num } ??
            $qm.prime-implicants{ 'table' ~ $table-num }.keys.join(', ') !!
            'None';

        # stop if the minterms cannot be combined further
        last if $should-stop;

        # print the corresponding table for the current groups
        print-table-for-groups(%groups);
    }

    sub print-table-for-groups( %groups ) {
        put "\n\nGroup No.\tMinterms\tBinary of Minterms\n{'=' x 50}";
        for %groups.keys.sort -> $i {
            "%5d: ".printf($i);
            for %groups{$i}.flat -> $j {
                printf("\t\t%-24s%s\n", find-combined-minterms($j).join(','), $j);
            }
            put '-' x 50 ;
        }
    }
}

#| Print the Prime Implicant Chart (PIC).
multi MAIN(
    'chart',
    Str :m($minterms) is required,
    Str :d($dont-care) = '',
    Bool :$show-epi = False,      #=« Show essential prime implicants (EPIs).
                                      False by default.»
) {

    my @m = $minterms.comb(/\d+/)».Int;
    my @d = $dont-care.comb(/\d+/)».Int;
    my @minterms = (@m, @d).flat;
    my $qm = QM.minterms(@m, @d);
 
    # The number of digits of the largest minterm
    my $sz = @m».Int.sort.tail.chars;
    my %chart = $qm.prime-implicants-chart;

    my $x = %chart.values.sort(*.elems)
            .tail.map({find-combined-minterms($_)}).Set
            .keys.join(',').chars;

    my $g = Generator.new(
        style => Style.new(
            corner-style => Style::Corner.single(),
            line-style => Style::Line.dot(),
            content-style => Style::Content.space(),
        )
    );

    $g.add-cell("Prime Implicant");
    $g.add-cell("Minterms Needing Coverage");
    $g.end-line;

    my $spc = ' ' x ($x+1);
    $g.add-cell("Prime Implicant");
    $g.add-cell(((' ' x $sz - $_.chars) ~ $_ for @m).join(' '));
    $g.end-line;

    my %implicants2minterms;

    my @PIs = $qm.prime-implicants<all>.keys.list;
    for @PIs -> $i {
        # get the minterms that needed to be combined to create minterm $i
        my @combined-minterms = find-combined-minterms($i);

        # print the combined minterms in a given row
        my @minterms-only = (@combined-minterms (-) @d).keys.sort;
        for @minterms-only -> $j {
            %implicants2minterms{@combined-minterms.join(',')}[$j] = 'X';
        }
    }

    for @PIs.sort(&by-pi-terms-number) -> $i {
        # get the minterms that needed to be combined to create minterm $i
        my @combined-minterms = find-combined-minterms($i);
        my $y = 0;

        # print the combined minterms in a given row
        my @minterms-only = (@combined-minterms ∖ @d).keys.sort;

        $g.add-cell(@minterms-only.join(','));

        my $row;
        for @minterms-only -> $j {
            my %values2index = (@minterms.values Z @minterms.keys).flat;
            my $x = %values2index{$j} *  ($sz+1); 

            $row ~= do if $show-epi and $i ∈ $qm.essential-prime-implicants and
               is-x-unique-in-column($j, %implicants2minterms)
            {
                (' ' x ($x-$y).abs, ' ' x ($sz-1), '▧').join
            }
            else {
                (' ' x ($x-$y).abs, ' ' x ($sz-1), 'X').join
            }

            $y = $x + $sz;
        }

        $g.add-cell($row);
        $g.end-line;
    }

    # print the table

    put "\nPrime Implicants Chart (PIC):\n";

    $g.generator().generate().print;

    print qq:to/END/;

    Prime Implicants: {$qm.prime-implicants<all>.keys.join(', ') }
    Essential Prime Implicants (EPI): { 
        $qm.essential-prime-implicants 
        ?? $qm.essential-prime-implicants.join(', ')
        !! 'None'
    }
    END

    sub by-pi-terms-number {
        (find-combined-minterms($^i) ∖ @d).keys.elems;
    }

    sub is-x-unique-in-column($column, %implicants2minterms) {
        %implicants2minterms.values».[$column].grep(* !=== Any).Int == 1;
    }
}

#| Print the reduced boolean expression for the given minterms.
multi MAIN( 'formula', Str :m($minterms) is required, Str :d($dont-care) = '' ) {
    my @m = $minterms.comb(/\d+/);
    my @d = $dont-care.comb(/\d+/);
    my $qm = QM.minterms(@m, @d);
    print qq:to/END/;

    Solution:
    F = {$qm.reduced-boolean-expression}
    END
}

sub USAGE {
    print qq:to/END/;
    Usage:
        {$*PROGRAM-NAME} -m=<Str> [-d=<Str>] all
            Print the group tables, the prime implicants chart and the reduced
            boolean expression for the given minterms.
        {$*PROGRAM-NAME} -m=<Str> [-d=<Str>] groups
            Print the group tables.
        {$*PROGRAM-NAME} -m=<Str> [-d=<Str>] [--show-epi] chart
            Print the Prime Implicant Chart (PIC).
        {$*PROGRAM-NAME} -m=<Str> [-d=<Str>] formula
            Print the reduced boolean expression for the given minterms.
      
        -m=<Str>         Space-separated list of minterms (e.g., -m='1 4 6 8')
        -d=<Str>         Space-separated list of don't care conditions
        --show-epi       Show essential prime implicants (EPIs) with ▧.
                         False by default.
    END
}
