use v6.d;
use RakuDoc::PromiseStrings;
use RakuDoc::Numeration;
use PrettyDump;

#| Instances of ProcessedState are created to contain the rendered form and collected data
#| of leaves of a RakuDoc AST.
class ProcessedState {
    #| String of rendered source, may contain Promises during rendering
    has PStr $.body is rw .= new;

    #| Table of Contents data
    #| Ordered array of { :level, :text, :target, :is-heading, :toc-type }
    #| level - in heading hierarchy, text - to be shown in TOC
    #| target - of item in text, is-heading - used for Index placing
    has Hash @.toc;

    #| Index (from X<> markup)
    #| Hash entry => Hash of :refs, :sub-index
    #| :index (maybe empty) is Hash of sub-entry => :target, :sub-index
    #| :refs is Array of (Hash :target, :place, :is-header)
    #| :target is for link, :place is section name
    #| :is-header because X<> in headings treated differently to ordinary text
    has %.index;

    #| Footnotes (from N<> markup)
    #| Ordered Array of :$text, :$retTarget, :$fnNumber, :$fnTarget
    #| text is content of footnote, fnNumber is footNote number
    #| fnTarget is link to rendered footnote
    #| retTarget is link to where footnote is defined to link back form footnote
    has @.footnotes;

    #| Quotations, array of Pairs, id => content
    has %.q-codes;

    #| Semantic blocks (which includes TITLE & SUBTITLE) can be hidden
    #| Hash of SEMANTIC => [ PStr | Str ]
    has Array %.semantics;

    #| An array of warnings is generated and then rendered by the warnings template
    #| The warning template, by default is called by the wrap-source template
    #| RakuDoc warnings are generated as specified in the RakuDoc v2 document.
    has @.warnings;

    #| An array of accumulated rendered items, added to body when next non-item block encountered
    has @.items;

    #| An array of accumulated rendered definitions, added to body when next non-defn block encountered
    has @.defns;

    #| Hash of definition => rendered value for definitions
    has %.definitions;

    #| Array to signal when one or more inline defn are made in a Paragraph
    has @.inline-defns;

    multi method gist(ProcessedState:U: ) { 'Undefined ProcessedState object' }

    multi method gist(ProcessedState:D: Int :$output = 300 ) {
        qq:to/GIST/;
        ProcessedState contains:
            semantics => { pretty-dump( %.semantics, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
            :indent('  '), :post-separator-spacing("\n  ") )  }
            toc => { pretty-dump(@.toc, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
               :indent('  '), :post-separator-spacing("\n  ")) }
            index => { pretty-dump( %.index, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
               :indent('  '), :post-separator-spacing("\n  ") )  }
            footnotes => { pretty-dump( @.footnotes, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") )  }
            warnings => { pretty-dump( @.warnings, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            items => { pretty-dump( @.items, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            definitions => { pretty-dump( %.definitions, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
               :indent('  '), :post-separator-spacing("\n  ") )  }
            defns => { pretty-dump( @.defns, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            inline-defns => { pretty-dump( @.inline-defns, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            body => { with $.body.debug  { .substr(0, $output) ~ ( .chars > $output ?? "\n... (" ~ .chars - $output ~ ' more chars)' !! '') } }
        GIST
    }
}

#| Class object contains the rendered data of a RakuDoc source
class RakuDoc::Processed is ProcessedState {
    #| Information about the RakuDoc source, eg file name, path, modified, language
    has %.source-data;
    #| Document level options
    has %.document-options =
        error => True,
        auto-toc => 'Table of Contents',
        auto-index => 'Index',
        auto-citations => 'Bibliography',
        citation-style => 'ieee',
        citation-locale => 'en-GB',
        lang => 'en',
        FrontMatter => 'Preface',
    ;
    #| The output format that the source has been rendered into
    has $!output-format = 'txt';
    #| Name to be used in titles and files
    #| name can be modified after creation of Object
    #| name can be set when creating object
    #| if name is not set, then it is taken from source name + format
    has Str $.name is rw;
    #| String value of TITLE.
    has Str $.title is rw = 'NO_TITLE';
    #| Target of Title Line
    has Str $.title-target is rw = '___top';
    #| String value of SUBTITLE, provides description of file
    has Str $.subtitle is rw = '';
    #| When RakuDoc Processed Object modified
    #| (source-data<modified> should be earlier than RPO.modified)
    has Str $.modified is rw = now.DateTime.truncated-to('seconds').Str;
    #| target data generated from block names and :id metadata
    #| A set of unique targets inside the file, new targets must be unique
    has SetHash $.targets;
    #| Links (from L<link-label|destination> markup)
    #| Hash of destination => :target, :type, :place, :link-label
    #| target = computed URL (for local files), place = anchor inside file
    #| type has following values
    #| Internal are of the form '#this is a heading' and refer to anchors inside the file
    #| Local are of the form 'some-type#a heading there', where 'some-type' is a file name in the same directory
    #| External is a fully qualified URL
    #|
    has Hash %.links;
    #| Rendered version of the ToC
    has $.rendered-toc is rw = '';
    #| Rendered version of the Index
    has $.rendered-index is rw = '';
    #| Citations data generated from citation blocks
    #| data are indexed by citation ids
    #| categories keys are category names and contain arrays of ids
    has %.citations =
        data => %(),
        categories => %(
            cited => SetHash.new,
            not-cited => SetHash.new
        ),
    ;
    #| for the autogenerated bibliography
    has $.rendered-citations is rw = '';

    submethod TWEAK( :%source-data, :$name, :$output-format ) {
        %!source-data =
            name => 'Unnamed-source',
            path => '.',
            modified => '2020-12-31T00:00:01Z',
            paragraph-id-length => 7,
        ;
        %!source-data{ .key } = .value for %source-data.pairs;
        $!output-format = $_ with $output-format;
        # if name is set on new, then it will be defined by TWEAK, else undefined, so take it from
        without $!name { $!name = %!source-data<name> ~ '.' ~ $!output-format }
        $!targets .= new;
    }
    
    multi method gist(RakuDoc::Processed:U: ) { 'Undefined RakuDoc::Processed object' }
    
    multi method gist(RakuDoc::Processed:D: Int :$output = 300 ) {
        qq:to/GIST/;
        RakuDoc source contains:
            name => Str=｢{ $!name }｣
            title => Str=｢{ $!title }｣
            title-target => Str=｢{ $!title-target }｣
            subtitle => Str=｢{ $!subtitle }｣
            source-data => { pretty-dump( %!source-data ) }
            semantics => { pretty-dump( %.semantics, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
            :indent('  '), :post-separator-spacing("\n  ") )  }
            toc => { pretty-dump(@.toc, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
               :indent('  '), :post-separator-spacing("\n  ")) }
            index => { pretty-dump( %.index, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
               :indent('  '), :post-separator-spacing("\n  ") )  }
            footnotes => { pretty-dump( @.footnotes, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") )  }
            links => { pretty-dump( %.links, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") )  }
            targets => <｢{ $!targets.keys.join('｣, ｢') }｣>
            items => { pretty-dump( @.items, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            definitions => { pretty-dump( %.definitions, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
               :indent('  '), :post-separator-spacing("\n  ") )  }
            defns => { pretty-dump( @.defns, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            inline-defns => { pretty-dump( @.inline-defns, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            warnings => { pretty-dump( @.warnings, :pre-item-spacing("\n   "),:post-item-spacing("\n    "),
                :indent('  '), :post-separator-spacing("\n  ") ) }
            body => { with $.body.debug  { .substr(0, $output) ~ ( .chars > $output ?? "\n... (" ~ .chars - $output ~ ' more chars)' !! '') } }
        GIST
    }
}
multi sub merge-index( %p, %q ) {
    for %q.keys -> $k {
        if %p{$k}:exists {
            %p{$k}<refs>.append: %q{$k}<refs>.Slip;
            if %p{$k}<sub-index>.elems and %q{$k}<sub-index>.elems {
                merge-index( %p{$k}<sub-index>, %q{$k}<sub-index> );
            }
            elsif %q{$k}<sub-index>.elems {
                %p{$k}<sub-index> = %q{$k}<sub-index>
            } # otherwise either p-subindex or q-sub-index are empty, so no change
        }
        else {
            %p{$k} = %q{$k}
        }
    }
}

#| Add one ProcessedState object to another
multi sub infix:<+>( ProcessedState $p, ProcessedState $q ) is export {
    sink $p.body ~ $q.body;
    $p.toc.append: $q.toc;
    merge-index($p.index, $q.index);
    $p.footnotes.append: $q.footnotes;
    for $q.semantics.kv -> $k, $v { # by definition, same key but multiple values
        $p.semantics{$k}.append: $v.Slip
    }
    $p.warnings.append: $q.warnings;
    $p.items.append: $q.items;
    $p.defns.append: $q.defns;
    $p.inline-defns.append: $q.inline-defns;
    for $q.definitions.kv -> $k, $v { # no multiple values
        $p.definitions{$k} = $v # redefinition possible
    }
    for $q.q-codes.kv -> $k, $v { # no multiple values
        $p.q-codes{$k} = $v # redefinition possible
    }
    $p
}