<?php

class td_resources_optimize {

    /**
     * Store the HTML content of the page.
     */
    private $content = '';




    /**
     * Flag to determine whether to minify css or not.
     */
    private $minify_css;


    /**
     * Flag to determine whether to aggregate inline css or not.
     */
    private $aggregate_inline_css;


    /**
     * Store the inline CSS.
     */
    private $inline_css = array();




    /**
     * Flag to determine whether we are in TagDiv Composer or not.
     */
    private $in_composer = false;




    /**
     * Class constructor.
     *
     */
    public function __construct() {

        /* -- Set options -- */
        $this->aggregate_inline_css = td_util::get_option('tds_aggregate_inline_css') == 'enabled';
        $this->minify_css = td_util::get_option('tds_minify_inline_css') == '';

        $this->in_composer = td_util::tdc_is_live_editor_iframe() || td_util::tdc_is_live_editor_ajax();


        /* -- Hooks -- */
        add_action( 'template_redirect', array( $this, 'start_html_buffering' ) );

    }




    /**
     * Setup page html output buffering.
     */
    public function start_html_buffering() {

        ob_start( array( $this, 'end_html_buffering' ) );

    }




    /**
     * Processes the output-buffered page html content and returns it.
     *
     * @param string $html
     *
     * @return string
     */
    private function end_html_buffering( string $html ) : string {

        $this->content = $html;


        /* -- Optimize inline styles -- */
        if( $this->minify_css || $this->aggregate_inline_css ) {
            $this->content = $this->optimize_styles( $this->content );
        }


        /* -- Load the tdScrollToClass.js file if the option is present -- */
        /* -- in the HTML content of the page -- */
        if( strpos( $this->content, 'data-scroll-to-class' ) !== false && !$this->in_composer ) {
            $this->content = $this->insert_in_html( '<script type="text/javascript" src="' . TDC_SCRIPTS_URL . '/tdScrollToClass.js' . TDC_SCRIPTS_VER . '" id="tdScrollToClass-js"></script>', array( '<!-- JS generated by theme -->', 'before' ), $this->content );
        }


        return $this->content;

    }




    /**
     * Processes all the inline css styles within a page html content and returns it.
     *
     * @param string $html
     *
     * @return string
     */
    private function optimize_styles( string $html ) : string {

        /* -- Search for inline style elements. -- */
        preg_match_all('#(?<opening_tag><style[^>]*>)(?<tag_contents>.*?)(?<closing_tag><\/style>|<\\\/style>)#si', $html, $inline_css_matches);

        // Bail if no results.
        if( !$inline_css_matches ) {
            return $html;
        }



        /* -- Process the results. -- */
        // Strings that the inline css blocks must contain
        // in order for the optimizer to know they are coming
        // from our theme/plugins.
        $inline_css_contains = array(
            '/* custom css - generated by TagDiv Composer */',
            '/* inline tdc_css att - generated by TagDiv Composer */',
            '/* inline css att - generated by TagDiv Composer */',
            '/* inline css - generated by TagDiv Composer */',
            '/* custom css - generated by TagDiv Theme Panel */',
            '/* custom css theme panel - generated by TagDiv Theme Panel */',
            '/* custom responsive css from theme panel (Advanced CSS) - generated by TagDiv Theme Panel */',
            '/* Style generated by theme for demo: '
        );


        for( $i = 0; $i < count( $inline_css_matches[0] ); $i++ ) {

            $full_tag = $inline_css_matches[0][$i];
            $opening_tag = $inline_css_matches['opening_tag'][$i];
            $closing_tag = $inline_css_matches['closing_tag'][$i];
            $tag_contents_original = $inline_css_matches['tag_contents'][$i];


            // Check if the inline style element contains one
            // of the required string set above.
            if( !$this->string_contains( $tag_contents_original, $inline_css_contains ) ) {
                continue;
            }


            // Minify the CSS inside the element?
            $tag_contents_processed = $this->minify_css ? $this->css_minifier( $tag_contents_original ) : $tag_contents_original;


            // Store the inline css
            $this->inline_css[] = $tag_contents_processed;


            // Replace the original style element's content, or completely
            // remove it if we want to aggregate it later on.
            if( $this->aggregate_inline_css ) {
                $html = str_replace( $full_tag, '', $html, $count );
            } else {
                $html = str_replace( $full_tag, $opening_tag . $tag_contents_processed . $closing_tag, $html, $count );
            }

        }


        // Aggregate the inline css?
        if( $this->aggregate_inline_css && !empty( $this->inline_css ) ) {

            $separator = !$this->minify_css ? PHP_EOL : '';
            $insert_inline_css = implode($separator, $this->inline_css);

            $html = $this->insert_in_html( '<style id="td-inline-css-aggregated">' . $insert_inline_css . '</style>', array( '</head>', 'before' ), $html );

        }



        /* -- Return the optimized html content. -- */
        return $html;

    }




    /**
     * HTML minifier.
     *
     * @param string $string
     *
     * @return string
     */
    public static function html_minifier( string $string = '' ) : string {

        $replace = array(
            //remove tabs before and after HTML tags
            '/\>[^\S ]+/s'                                                      => '>',
            '/[^\S ]+\</s'                                                      => '<',
            //shorten multiple whitespace sequences; keep new-line characters because they matter in JS!!!
            '/([\t ])+/s'                                                       => ' ',
            //remove leading and trailing spaces
            '/^([\t ])+/m'                                                      => '',
            '/([\t ])+$/m'                                                      => '',
            // remove JS line comments (simple only); do NOT remove lines containing URL (e.g. 'src="http://server.com/"')!!!
            '~//[a-zA-Z0-9 ]+$~m'                                               => '',
            //remove empty lines (sequence of line-end and white-space characters)
            '/[\r\n]+([\t ]?[\r\n]+)+/s'                                        => "\n",
            //remove empty lines (between HTML tags); cannot remove just any line-end characters because in inline JS they can matter!
            '/\>[\r\n\t ]+\</s'                                                 => '><',
            //remove "empty" lines containing only JS's block end character; join with next line (e.g. "}\n}\n</script>" --> "}}</script>"
            '/}[\r\n\t ]+/s'  => '}',
            '/}[\r\n\t ]+,[\r\n\t ]+/s'                                         => '},',
            //remove new-line after JS's function or condition start; join with next line
            '/\)[\r\n\t ]?{[\r\n\t ]+/s'                                        => '){',
            '/,[\r\n\t ]?{[\r\n\t ]+/s'                                         => ',{',
            //remove new-line after JS's line end (only most obvious and safe cases)
            '/\),[\r\n\t ]+/s'                                                  => '),',
            //remove quotes from HTML attributes that does not contain spaces; keep quotes around URLs!
            '~([\r\n\t ])?([a-zA-Z0-9]+)="([a-zA-Z0-9_/\\-]+)"([\r\n\t ])?~s'   => '$1$2=$3$4', //$1 and $4 insert first white-space character found before/after attribute
        );


        return trim( preg_replace( array_keys($replace), array_values($replace), $string ) );

    }




    /**
     * CSS minifier.
     *
     * @param string $string
     *
     * @return string
     */
    public static function css_minifier( string $string = '' ) : string {

        $comments = <<<'EOS'
            (?sx)
                # don't change anything inside of quotes
                ( "(?:[^"\\]++|\\.)*+" | '(?:[^'\\]++|\\.)*+' )
            |
                # comments
                /\* (?> .*? \*/ )
            EOS;

        $everything_else = <<<'EOS'
            (?six)
                # don't change anything inside of quotes
                ( "(?:[^"\\]++|\\.)*+" | '(?:[^'\\]++|\\.)*+' )
            |
                # spaces before and after ; and }
                \s*+ ; \s*+ ( } ) \s*+
            |
                # all spaces around meta chars/operators (excluding + and -)
                \s*+ ( [*$~^|]?+= | [{};,>~] | !important\b ) \s*+
            |
                # all spaces around + and - (in selectors only!)
                \s*([+-])\s*(?=[^}]*{)
            |
                # spaces right of ( [ :
                ( [[(:] ) \s++
            |
                # spaces left of ) ]
                \s++ ( [])] )
            |
                # spaces left (and right) of : (but not in selectors)!
                \s+(:)(?![^\}]*\{)
            |
                # spaces at beginning/end of string
                ^ \s++ | \s++ \z
            |
                # double spaces to single
                (\s)\s+
            EOS;


        $search_patterns  = array( "%{$comments}%", "%{$everything_else}%" );
        $replace_patterns = array( '$1', '$1$2$3$4$5$6$7$8' );


        return preg_replace( $search_patterns, $replace_patterns, $string );

    }




    /**
     * Inserts the given payload intro the page html at
     * the specified location.
     *
     * @param string $payload
     * @param array  $where
     * @param string $content
     *
     * @return string
     */
    public static function insert_in_html( string $payload, array $where, string $content ) : string {

        /* -- Find the position of the string where to -- */
        /* -- insert the payload before/after -- */
        $position = strpos( $content, $where[0] );

        if( !$position ) {
            return $content;
        }


        /* -- Prepare the payload -- */
        if( $where[1] == 'after' ) {
            $insert_content = $where[0] . $payload;
        } else if( $where[1] == 'replace' ) {
            $insert_content = $payload;
        } else {
            $insert_content = $payload . $where[0];
        }


        /* -- Insert the payload -- */
        return substr_replace(
            $content,
            $insert_content,
            $position,
            strlen( $where[0] )
        );

    }




    /**
     * Check if a string contains a substring from an array.
     *
     * @param string $string
     * @param array $substrings
     *
     * @return bool
     */
    private function string_contains( string $string, array $substrings ) : bool {

        foreach( $substrings as $substring ) {
            if( strpos( $string, $substring ) !== false ) {
                return true;
            }
        }

        return false;

    }

}
new td_resources_optimize();