package LatexIndent::Heading;

#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	See http://www.gnu.org/licenses/.
#
#	Chris Hughes, 2017-2025
#
#	For all communication, please visit: https://github.com/cmhughes/latexindent.pl
use strict;
use warnings;
use Data::Dumper;
use LatexIndent::Tokens           qw/%tokens/;
use LatexIndent::Switches         qw/$is_m_switch_active $is_t_switch_active $is_tt_switch_active/;
use LatexIndent::TrailingComments qw/$trailingCommentRegExp/;
use LatexIndent::GetYamlSettings  qw/%mainSetting %previouslyFoundSetting/;
use LatexIndent::LogFile          qw/$logger/;
use Exporter                      qw/import/;
our @ISA = "LatexIndent::Document";    # class inheritance, Programming Perl, pg 321
our @EXPORT_OK
    = qw/find_heading construct_headings_levels $allHeadingsRegexp @headingsRegexpArray after_heading_indentation/;
our $headingCounter;
our @headingsRegexpArray;
our $allHeadingsRegexp = q();
our %headingLevelLookUp;

sub construct_headings_levels {
    my $self = shift;

    # grab the heading levels
    my %headingsLevels = %{ $mainSetting{indentAfterHeadings} };

    # output to log file
    $logger->trace("*Constructing headings reg exp for example, chapter, section, etc (see indentAfterThisHeading)")
        if $is_t_switch_active;

    my $oldYAMLHeadingSyntax = 0;
    while ( my ( $headingName, $headingInfo ) = each %headingsLevels ) {
        if ( defined ${ $headingsLevels{$headingName} }{indentAfterThisHeading} ) {
            ${ $headingsLevels{$headingName} }{lookForThis}
                = ${ $headingsLevels{$headingName} }{indentAfterThisHeading};
            delete ${ $headingsLevels{$headingName} }{indentAfterThisHeading};
            $oldYAMLHeadingSyntax = 1;
        }
    }
    if ($oldYAMLHeadingSyntax) {
        $logger->warn("*Obsolete indentAfterHeadings YAML syntax used, changed to the following:");
        $logger->warn( Dumper( \%headingsLevels ) );
    }

    # delete the values that have lookForThis set to 0
    while ( my ( $headingName, $headingInfo ) = each %headingsLevels ) {
        if ( !${ $headingsLevels{$headingName} }{lookForThis} ) {
            $logger->trace("Not indenting after $headingName (see lookForThis)") if $is_t_switch_active;
            delete $headingsLevels{$headingName};
        }
        else {
            # log the heading and level
            $headingLevelLookUp{$headingName} = ${ $headingsLevels{$headingName} }{level};

            # *all heading* regexp, remembering to put starred headings at the front of the regexp
            if ( $headingName =~ m/\*/ ) {
                $logger->trace("Putting $headingName at the beginning of the allHeadings regexp, as it contains a *")
                    if $is_t_switch_active;
                $allHeadingsRegexp = $headingName . ( $allHeadingsRegexp eq '' ? q() : "|$allHeadingsRegexp" );
            }
            else {
                $logger->trace("Putting $headingName at the END of the allHeadings regexp") if $is_t_switch_active;
                $allHeadingsRegexp .= ( $allHeadingsRegexp eq '' ? q() : "|" ) . $headingName;
            }
        }
    }

    # check for a * in the name
    $allHeadingsRegexp =~ s/\*/\\\*/g;

    # sort the file extensions by preference
    my @sortedByLevels = sort { ${ $headingsLevels{$a} }{level} <=> $headingsLevels{$b}{level} } keys(%headingsLevels);

    # it could be that @sortedByLevels is empty;
    return if !@sortedByLevels;

    $logger->trace("All headings regexp: $allHeadingsRegexp")           if $is_t_switch_active;
    $logger->trace("*Now to construct headings regexp for each level:") if $is_t_switch_active;

# loop through the levels, and create a regexp for each (min and max values are the first and last values respectively from sortedByLevels)
    for (
        my $i = ${ $headingsLevels{ $sortedByLevels[0] } }{level};
        $i <= ${ $headingsLevels{ $sortedByLevels[-1] } }{level};
        $i++
        )
    {
        # level regexp
        my @tmp = grep { ${ $headingsLevels{$_} }{level} == $i } keys %headingsLevels;
        if (@tmp) {
            my $headingsAtThisLevel = q();
            foreach (@tmp) {

                # put starred headings at the front of the regexp
                if ( $_ =~ m/\*/ ) {
                    $logger->trace("Putting $_ at the beginning of this regexp (level $i), as it contains a *")
                        if $is_t_switch_active;
                    $headingsAtThisLevel = $_ . ( $headingsAtThisLevel eq '' ? q() : "|$headingsAtThisLevel" );
                }
                else {
                    $logger->trace("Putting $_ at the END of this regexp (level $i)") if $is_t_switch_active;

                    # note: NOT followed by a * (gets escaped next)
                    $headingsAtThisLevel .= ( $headingsAtThisLevel eq '' ? q() : "|" ) . $_ . "(?!*)";
                }
            }

            # make the stars escaped correctly
            $headingsAtThisLevel =~ s/\*/\\\*/g;
            push( @headingsRegexpArray, $headingsAtThisLevel );
            $logger->trace("Heading level regexp for level $i is: $headingsAtThisLevel")
                if $is_t_switch_active;
        }
    }
}

sub find_heading {

    # if there are no headings regexps, there's no point going any further
    my $self = shift;
    $self->construct_headings_levels;
    return if !@headingsRegexpArray;

    # otherwise loop through the headings regexp
    $logger->trace("*Searching ${$self}{name} for headings with following levels (see indentAfterHeadings)")
        if $is_t_switch_active;
    $logger->trace( Dumper( \%headingLevelLookUp ) ) if $is_t_switch_active;

    # preamble has already been indented, so now make it verbatim
    if ( $mainSetting{indentPreamble} ) {
        $logger->trace("*protecting preamble which can contain headings commands (indentPreamble: 1)");
        $mainSetting{indentPreamble} = 0;
        $self->find_file_contents_environments_and_preamble;
        ${$self}{preambleIndentationWanted} = 1;
    }

    my @bodyParts = &headings_get_body_parts( ${$self}{body} );

    my $newBody = q();
    foreach (@bodyParts) {
        ${$_}{body} = &after_heading_indentation( ${$_}{body}, 0 );
        $newBody .= ${$_}{body};
    }

    # loop through each level of headings, starting at level 0
    ${$self}{body} = $newBody;

    $logger->trace("*headings indentation complete (see indentAfterHeadings)") if $is_t_switch_active;
    $self->put_verbatim_back_in( match => "preamble" )                         if ${$self}{preambleIndentationWanted};
}

sub headings_get_body_parts {
    my $body = $_[0];

    # create appropriately headed body parts
    my $index = -1;
    my @bodyParts;
    my $currentHeadingLevel = 0;
    my $headingValue;
    foreach ( split( qr/(\\($allHeadingsRegexp))/, $body ) ) {
        $index++;

        # first entry is *before* heading, so nothing to do
        if ( $index == 0 ) {
            push( @bodyParts, { body => $_ } );
            next;
        }

        # very first match gets appended to first "body part"
        if ( $index == 1 or $index == 3 ) {
            ${ $bodyParts[0] }{body} .= $_;
            next;
        }

        # very first previous heading
        if ( $index == 2 ) {
            ${ $bodyParts[0] }{level} = $headingLevelLookUp{$_};
            next;
        }

        #-------------------
        # heading value
        if ( $index % 3 == 1 ) {
            $headingValue = $_;
            next;
        }

        # heading level
        if ( $index % 3 == 2 ) {
            $currentHeadingLevel = $headingLevelLookUp{$_};
            next;
        }

        if ( $currentHeadingLevel >= ${ $bodyParts[-1] }{level} ) {
            ${ $bodyParts[-1] }{body} .= $headingValue . $_;
        }
        else {
            push( @bodyParts, { body => $headingValue . $_, level => $currentHeadingLevel } );
        }
    }

    return @bodyParts;
}

sub after_heading_indentation {
    my $body         = $_[0];
    my $currentLevel = $_[1];

    return $body unless defined $headingsRegexpArray[$currentLevel];

    my $headingRegExp = qr/(\\($headingsRegexpArray[$currentLevel]))/;

    # skip to the next level if there's no headings at this level
    if ( $body !~ m/$headingRegExp/s ) {
        $currentLevel++;
        $body = &after_heading_indentation( $body, $currentLevel );
        return $body;
    }

    # split body by heading regex
    my @newBody = split( $headingRegExp, $body );

    my $headingValue;
    my $name;
    my $index = -1;
    foreach (@newBody) {
        $index++;

        # first entry is *before* heading, so nothing to do
        next if $index == 0;

        # heading value
        if ( $index % 3 == 1 ) {
            $headingValue = $_;
            next;
        }

        # heading name
        if ( $index % 3 == 2 ) {
            $name = $_;
            $_    = "";
            $logger->trace("*found: $name heading") if $is_t_switch_active;
            next;
        }

        # get (sub) body parts
        my $newBodyPart = $_;

        my @bodyParts = &headings_get_body_parts($_);
        if ( scalar(@bodyParts) > 1 ) {
            $newBodyPart = q();
            my $bodyPartCount = -1;
            foreach my $bodyPart (@bodyParts) {
                $bodyPartCount++;
                $bodyPart = ${$bodyPart}{body};
                $bodyPart = &after_heading_indentation( $bodyPart, 0 ) unless $bodyPartCount == $#bodyParts;
                $newBodyPart .= $bodyPart;
            }
        }

        $_ = $newBodyPart;

        # look for next level heading
        $currentLevel++;
        $_ = &after_heading_indentation( $_, $currentLevel );
        $currentLevel--;

        # heading body indentation
        my $codeBlockObj;
        my $modifyLineBreaksName = "afterHeading";
        if ( !$previouslyFoundSetting{ $name . $modifyLineBreaksName } ) {

            $codeBlockObj = LatexIndent::Blocks->new(
                name                       => $name,
                nameForIndentationSettings => $name,
                modifyLineBreaksYamlName   => $modifyLineBreaksName,
                type                       => "heading",
            );
            if ( defined ${ ${ $mainSetting{indentAfterHeadings} }{$name} }{blocksEndBefore} ) {
                ${$codeBlockObj}{blocksEndBefore}
                    = ${ ${ $mainSetting{indentAfterHeadings} }{$name} }{blocksEndBefore};
            }
            $codeBlockObj->yaml_get_indentation_settings_for_this_object;
        }

        if ( ${ $previouslyFoundSetting{ $name . $modifyLineBreaksName } }{indentation} ne '' ) {
            my $addedIndentation = ${ $previouslyFoundSetting{ $name . $modifyLineBreaksName } }{indentation};

            # some heading blocks end before something specific (see headings-single-line-mod1.tex)
            my @afterBlocksEndBefore = ( q(), q(), q() );
            if ( defined ${ $previouslyFoundSetting{ $name . $modifyLineBreaksName } }{blocksEndBefore} ) {
                @afterBlocksEndBefore
                    = split( qr/(${$previouslyFoundSetting{$name.$modifyLineBreaksName}}{blocksEndBefore})/, $_ );
                $_ = $afterBlocksEndBefore[0];
            }

            # add indentation
            $_ =~ s"^"$addedIndentation"mg;
            $_ =~ s"^$addedIndentation""s;
            $_ =~ s"\R$addedIndentation(\h*)$"\n$1"s;
            $_ .= ( defined $afterBlocksEndBefore[1] ? $afterBlocksEndBefore[1] : q() )
                . ( defined $afterBlocksEndBefore[2] ? $afterBlocksEndBefore[2] : q() );
        }
    }

    $body = join( "", @newBody );
    return $body;
}

1;
