package LatexIndent::Wrap;
#	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
#
#	For all communication, please visit: https://github.com/cmhughes/latexindent.pl
use strict;
use warnings;
use Text::Wrap;
use LatexIndent::Tokens qw/%tokens/;
use LatexIndent::TrailingComments qw/$trailingCommentRegExp/;
use LatexIndent::GetYamlSettings qw/%mainSettings/;
use LatexIndent::Switches qw/$is_t_switch_active $is_tt_switch_active $is_m_switch_active/;
use LatexIndent::LogFile qw/$logger/;
use LatexIndent::Verbatim qw/%verbatimStorage/;
use Exporter qw/import/;
our @ISA = "LatexIndent::Document"; # class inheritance, Programming Perl, pg 321
our @EXPORT_OK = qw/text_wrap/;
our $sentenceCounter;

sub text_wrap{
    my $self = shift;

    $logger->trace("*Text wrap regular expression construction: (see textWrapOptions: )") if $is_t_switch_active;

    # textWrap Blocks FOLLOW
    # textWrap Blocks FOLLOW
    # textWrap Blocks FOLLOW
    my $blocksFollow = q();
    my $blocksFollowHash = \%{${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksFollow}};

    foreach my $blocksFollowEachPart (sort keys %{$blocksFollowHash}) {
        my $yesNo = $blocksFollowHash->{$blocksFollowEachPart};
        if($yesNo){
            if($blocksFollowEachPart eq "par"){
                $blocksFollowEachPart = qr/\R?\\par/s;
            } elsif ($blocksFollowEachPart eq "blankLine"){
                $blocksFollowEachPart = qr/
                        (?:\A(?:$tokens{blanklines}\R)+)     # the order of each of these 
                                |                            # is important, as (like always) the first
                        (?:\G(?:$tokens{blanklines}\R)+)     # thing to be matched will 
                                |                            # be accepted
                        (?:(?:$tokens{blanklines}\h*\R)+)
                                |
                                \R{2,}
                                |
                                \G
                        /sx;
            } elsif ($blocksFollowEachPart eq "commentOnPreviousLine"){
                $blocksFollowEachPart = qr/^$trailingCommentRegExp/m;
            } elsif ($blocksFollowEachPart eq "verbatim"){
                $blocksFollowEachPart = qr/$tokens{verbatim}\d+$tokens{endOfToken}/;
            } elsif ($blocksFollowEachPart eq "filecontents"){
                $blocksFollowEachPart = qr/$tokens{filecontents}\d+$tokens{endOfToken}/;
            } elsif ($blocksFollowEachPart eq "headings"){
                $blocksFollowEachPart = q();

                my %headingsLevels = %{$mainSettings{indentAfterHeadings}};
                while( my ($headingName,$headingInfo)= each %headingsLevels){
                   # check for a * in the name
                   $headingName =~ s/\*/\\\*/g;

                   # make the regex as 
                   #
                   #    headingName
                   #     <hspace>   # possible horizontal space
                   #     []?        # possible optional argument
                   #     <hspace>   # possible horizontal space
                   #     {}         #          mandatory argument
                   #
                   $headingName = qr/\\$headingName\h*(?:\[[^]]*?\])?\h*\{[^}]*?\}\h*(?:\\label\{[^}]*?\})?/m;

                   # put starred headings at the front of the regexp
                   if($headingName =~ m/\*/){
                        $blocksFollowEachPart = $headingName.($blocksFollowEachPart eq '' ?q():"|$blocksFollowEachPart");
                   } else {
                        $blocksFollowEachPart .= ($blocksFollowEachPart eq '' ?q():"|").$headingName ;
                   }
                }
            } elsif ($blocksFollowEachPart eq "other"){
                $blocksFollowEachPart = qr/$yesNo/x;
            }
            $blocksFollow .= ($blocksFollow eq '' ? q() : "|").qr/$blocksFollowEachPart/sx;
        }
    }

    # if blankLine is not active from blocksFollow then we need to set up the 
    # beginning of the string, but make sure that it is *not* followed by a 
    # blank line token, or a blank line
    if(!${${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksFollow}}{blankLine}){
            $blocksFollow .= ($blocksFollow eq '' ? q() : "|").
                                    qr/
                                        \G
                                        (?!$tokens{blanklines})
                                    /sx;
    }

    # followed by 0 or more h-space and line breaks
    $blocksFollow = ($blocksFollow eq '' ? q() : qr/(?:$blocksFollow)(?:\h|\R)*/sx );

    $logger->trace("textWrap blocks follow regexp:") if $is_tt_switch_active;
    $logger->trace($blocksFollow) if $is_tt_switch_active;
    
    # textWrap Blocks BEGIN with
    # textWrap Blocks BEGIN with
    # textWrap Blocks BEGIN with
    my $blocksBeginWith = q();
    my $blocksBeginWithHash = \%{${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksBeginWith}};

    foreach my $blocksBeginWithEachPart (sort keys %{$blocksBeginWithHash}) {
        my $yesNo = $blocksBeginWithHash->{$blocksBeginWithEachPart};
        if($yesNo){
            if($blocksBeginWithEachPart eq "A-Z"){
                $logger->trace("textWrap Blocks BEGINS with capital letters (see textWrap:blocksBeginWith:A-Z)") if $is_t_switch_active;
                $blocksBeginWithEachPart = qr/(?!(?:$tokens{blanklines}|$tokens{verbatim}|$tokens{preamble}))[A-Z]/;
            } elsif ($blocksBeginWithEachPart eq "a-z"){
                $logger->trace("textWrap Blocks BEGINS with lower-case letters (see textWrap:blocksBeginWith:a-z)") if $is_t_switch_active;
                $blocksBeginWithEachPart = qr/[a-z]/;
            } elsif ($blocksBeginWithEachPart eq "0-9"){
                $logger->trace("textWrap Blocks BEGINS with numbers (see textWrap:blocksBeginWith:0-9)") if $is_t_switch_active;
                $blocksBeginWithEachPart = qr/[0-9]/;
            } elsif ($blocksBeginWithEachPart eq "other"){
                $logger->trace("textWrap Blocks BEGINS with other $yesNo (reg exp) (see textWrap:blocksBeginWith:other)") if $is_t_switch_active;
                $blocksBeginWithEachPart = qr/$yesNo/;
            }
            $blocksBeginWith .= ($blocksBeginWith eq "" ? q(): "|" ).$blocksBeginWithEachPart;
        }
    }
    $blocksBeginWith = qr/$blocksBeginWith/;

    # textWrap Blocks END with
    # textWrap Blocks END with
    # textWrap Blocks END with
    my $blocksEndBefore = q();
    my $blocksEndBeforeHash = \%{${$mainSettings{modifyLineBreaks}{textWrapOptions}}{blocksEndBefore}};

    foreach my $blocksEndBeforeEachPart (sort keys %{$blocksEndBeforeHash}) {
        my $yesNo = $blocksEndBeforeHash->{$blocksEndBeforeEachPart};
        if($yesNo){
            if ($blocksEndBeforeEachPart eq "other"){
                $logger->trace("textWrap Blocks ENDS with other $yesNo (reg exp) (see textWrap:blocksEndBefore:other)") if $is_t_switch_active;
                $blocksEndBeforeEachPart = qr/\R?$yesNo/;
            } elsif ($blocksEndBeforeEachPart eq "commentOnOwnLine"){
                $logger->trace("textWrap Blocks ENDS with commentOnOwnLine (see textWrap:blocksEndBefore:commentOnOwnLine)") if $is_t_switch_active;
                $blocksEndBeforeEachPart = qr/^$trailingCommentRegExp/m;
            } elsif ($blocksEndBeforeEachPart eq "verbatim"){
                $logger->trace("textWrap Blocks ENDS with verbatim (see textWrap:blocksEndBefore:verbatim)") if $is_t_switch_active;
                $blocksEndBeforeEachPart = qr/$tokens{verbatim}\d/;
            } elsif ($blocksEndBeforeEachPart eq "filecontents"){
                $logger->trace("textWrap Blocks ENDS with filecontents (see textWrap:blocksEndBefore:filecontents)") if $is_t_switch_active;
                $blocksEndBeforeEachPart = qr/$tokens{filecontents}/;
            }
            $blocksEndBefore .= ($blocksEndBefore eq "" ? q(): "|" ).$blocksEndBeforeEachPart;
        }
    }
    $blocksEndBefore = qr/$blocksEndBefore/;

    # the OVERALL textWrap Blocks regexp
    # the OVERALL textWrap Blocks regexp
    # the OVERALL textWrap Blocks regexp
    $logger->trace("Overall textWrap Blocks end with regexp:") if $is_tt_switch_active;
    $logger->trace($blocksEndBefore) if $is_tt_switch_active;

    # store the text wrap blocks
    my @textWrapBlockStorage = split(/($blocksFollow)/,${$self}{body});

    # sentences need special treatment
    if (${$self}{modifyLineBreaksYamlName} eq 'sentence'){
        @textWrapBlockStorage = (${$self}{body});
    }

    # call the text wrapping routine
    my $columns = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{columns};

    # vital Text::Wrap options
    $Text::Wrap::columns=$columns;
    $Text::Wrap::huge = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{huge};

    # all other Text::Wrap options not usually needed/helpful, but available
    $Text::Wrap::separator = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{separator} if(${$mainSettings{modifyLineBreaks}{textWrapOptions}}{separator} ne '');
    $Text::Wrap::break     = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{break}     if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{break};
    $Text::Wrap::unexpand  = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{unexpand}  if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{unexpand};
    $Text::Wrap::tabstop   = ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{tabstop}   if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{tabstop};

    # clear the body, which will be updated with newly wrapped body
    ${$self}{body} = q();

    # loop back through the text wrap block storage 
    foreach my $textWrapBlockStorageValue (@textWrapBlockStorage){
      if ($textWrapBlockStorageValue =~ m/^\s*$blocksBeginWith/s or ${$self}{modifyLineBreaksYamlName} eq 'sentence') {

        # LIMIT is one greater than the maximum number of times EXPR may be split
        my @textWrapBeforeEndWith = split(/($blocksEndBefore)/,$textWrapBlockStorageValue,2);
        
        # sentences need special treatment
        if (${$self}{modifyLineBreaksYamlName} eq 'sentence'){
            @textWrapBeforeEndWith = ();
        }

        # if we have an occurence of blocksEndBefore, then grab the stuff before it
        if (scalar @textWrapBeforeEndWith > 1){
            $textWrapBlockStorageValue = $textWrapBeforeEndWith[0];
        } 

        $logger->trace("TEXTWRAP BLOCK: $textWrapBlockStorageValue") if $is_tt_switch_active;

        # initiate the trailing comments storage
        my $trailingComments = q();

        # grab all *internal* trailing comments from the block
        while($textWrapBlockStorageValue =~ m|$trailingCommentRegExp|){
            $textWrapBlockStorageValue =~ s|(\h*$trailingCommentRegExp)||s;
            $trailingComments .= $1;
        }

        # remove internal line breaks
        $textWrapBlockStorageValue =~ s/\R(?!\Z)/ /sg; 

        # convert multiple spaces into single
        $textWrapBlockStorageValue =~ s/\h{2,}/ /sg if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{multipleSpacesToSingle};

        # goal: get an accurate measurement of verbatim objects;
        #
        # example:
        #       Lorem \verb!x+y! ipsum dolor sit amet
        #
        # is represented as
        #
        #       Lorem LTXIN-TK-VERBATIM1-END ipsum dolor sit amet
        #
        # so we *measure* the verbatim token and replace it with
        # an appropriate-length string
        #
        #       Lorem a2A41233rt ipsum dolor sit amet
        #
        # and then put the body back to
        #
        #       Lorem LTXIN-TK-VERBATIM1-END ipsum dolor sit amet
        #
        # following the text wrapping
        my @putVerbatimBackIn;

        # check body for verbatim and get measurements
        if ($textWrapBlockStorageValue =~ m/$tokens{verbatim}/s and ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{huge} eq "overflow"){

          # reference: https://stackoverflow.com/questions/10336660/in-perl-how-can-i-generate-random-strings-consisting-of-eight-hex-digits
          my @set = ('0' ..'9', 'A' .. 'Z', 'a' .. 'z');

          # loop through verbatim objects
          while( my ($verbatimID,$child)= each %verbatimStorage){
            my $verbatimThing = ${$child}{begin}.${$child}{body}.${$child}{end};

            # if the object has line breaks, don't measure it
            next if $verbatimThing =~ m/\R/s;

            if($textWrapBlockStorageValue =~m/$verbatimID/s){

              # measure length
              my $verbatimLength = Unicode::GCString->new($verbatimThing)->columns();

              # create temporary ID, and check that it is not contained in the body
              my $verbatimTmpID = join '' => map $set[rand @set], 1 .. $verbatimLength;
              while($textWrapBlockStorageValue=~m/$verbatimTmpID/s){
                 $verbatimTmpID = join '' => map $set[rand @set], 1 .. $verbatimLength;
              }

              # store for use after the text wrapping
              push(@putVerbatimBackIn,{origVerbatimID=>$verbatimID,tmpVerbatimID=>$verbatimTmpID});

              # make the substitution
              $textWrapBlockStorageValue =~ s/$verbatimID/$verbatimTmpID/s;
            }
          }
        }

        # perform the text wrap routine
        $textWrapBlockStorageValue = wrap('','',$textWrapBlockStorageValue) if ${$mainSettings{modifyLineBreaks}{textWrapOptions}}{columns} > 0;

        # append trailing comments from WITHIN the block
        $textWrapBlockStorageValue =~ s/\R?$/$trailingComments\n/s if ($trailingComments ne '');

        # append blocksEndBefore and the stuff following it
        if (scalar @textWrapBeforeEndWith > 1){
            $textWrapBlockStorageValue .= $textWrapBeforeEndWith[1].$textWrapBeforeEndWith[2];
        } 

        # put blank line tokens on their own lines, should only happen when the following is used:
        #   blocksFollow:
        #      blankLine: 0
        while ($textWrapBlockStorageValue =~ m/^\H.*?$tokens{blanklines}/m ){
            $textWrapBlockStorageValue =~ s/^(\H.*?)$tokens{blanklines}/$1\n$tokens{blanklines}/m; 
        }

        # remove surrounding spaces from blank line tokens
        $textWrapBlockStorageValue =~ s/^\h*$tokens{blanklines}\h*/$tokens{blanklines}/mg; 

        # put verbatim back in
        $textWrapBlockStorageValue=~ s/${$_}{tmpVerbatimID}/${$_}{origVerbatimID}/s foreach (@putVerbatimBackIn);
      }

      # update the body
      ${$self}{body} .= $textWrapBlockStorageValue; 
    }

}

1;
