<?php
/**
 * Lightweight, dependency-free inline-SVG charts for the admin analytics views.
 *
 * Two renderers share one visual language (brand colour, gridlines, axis
 * labels, hover highlight): a filled area/line chart for trends with many
 * points (Dashboard) and a slim column chart for fewer, discrete buckets
 * (Visitors). No JS library, no external request — the markup is pure SVG so
 * it renders identically in a screenshot, an e-mail-safe context, or print.
 *
 * Input is a flat list of points:
 *   array(
 *     array( 'label' => 'Mon', 'value' => 248, 'tip' => 'Mon · 248 visits' ),
 *     ...
 *   )
 * 'tip' is optional (falls back to "label: value").
 *
 * @package Brainwerk SEO Suite
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class Seoforge_Charts {

    /** Per-request counter so each chart's <defs> gradient id is unique. */
    private static $seq = 0;

    /** Shared SVG geometry. Width is a viewBox unit; the SVG scales to 100%. */
    const W     = 760;
    const H     = 240;
    const PAD_L = 44;  // room for the y-axis value labels
    const PAD_R = 14;
    const PAD_T = 16;
    const PAD_B = 30;  // room for the x-axis labels

    /**
     * Filled area / line chart. Best for a continuous trend (daily/hourly
     * visits over a range).
     *
     * @param array $points list of ['label','value','tip'].
     * @param array $opts   optional: 'height' (int), 'unit' (string label).
     * @return string SVG markup (already escaped/safe to echo).
     */
    public static function area( array $points, array $opts = array() ) {
        $points = array_values( $points );
        $n      = count( $points );
        if ( 0 === $n ) {
            return '';
        }
        $geo    = self::geometry( $points, $opts );
        $id     = 'sf-area-' . ( ++self::$seq );
        $plot_w = $geo['plot_w'];
        $plot_h = $geo['plot_h'];
        $max    = $geo['max'];

        // Build the polyline / area coordinates.
        $step  = $n > 1 ? $plot_w / ( $n - 1 ) : 0;
        $line  = '';
        $dots  = '';
        $coords = array();
        foreach ( $points as $i => $p ) {
            $val = max( 0, (float) $p['value'] );
            $x   = self::PAD_L + ( $n > 1 ? $i * $step : $plot_w / 2 );
            $y   = self::PAD_T + $plot_h * ( 1 - $val / $max );
            $coords[] = array( $x, $y );
            $line    .= ( '' === $line ? 'M' : 'L' ) . self::num( $x ) . ' ' . self::num( $y ) . ' ';
        }
        // Closed area path: line, then down to baseline and back.
        $base_y = self::PAD_T + $plot_h;
        $area   = $line
            . 'L' . self::num( $coords[ $n - 1 ][0] ) . ' ' . self::num( $base_y ) . ' '
            . 'L' . self::num( $coords[0][0] ) . ' ' . self::num( $base_y ) . ' Z';

        // Hover dots (last point always emphasised).
        foreach ( $points as $i => $p ) {
            list( $x, $y ) = $coords[ $i ];
            $is_last = ( $i === $n - 1 );
            $dots   .= sprintf(
                '<circle class="sf-chart-dot%s" cx="%s" cy="%s" r="%s"><title>%s</title></circle>',
                $is_last ? ' is-last' : '',
                self::num( $x ),
                self::num( $y ),
                $is_last ? '4' : '3',
                esc_html( self::tip( $p ) )
            );
        }

        $svg  = self::open_svg( $opts, 'sf-chart-area' );
        $svg .= '<defs><linearGradient id="' . esc_attr( $id ) . '" x1="0" y1="0" x2="0" y2="1">'
            . '<stop offset="0%" class="sf-chart-grad-top"/>'
            . '<stop offset="100%" class="sf-chart-grad-bot"/></linearGradient></defs>';
        $svg .= self::grid_and_axis( $geo );
        $svg .= '<path class="sf-chart-fill" d="' . esc_attr( $area ) . '" fill="url(#' . esc_attr( $id ) . ')"/>';
        $svg .= '<path class="sf-chart-line" d="' . esc_attr( $line ) . '" vector-effect="non-scaling-stroke"/>';
        $svg .= $dots;
        $svg .= '</svg>';
        return $svg;
    }

    /**
     * Slim column chart. Best for a small set of discrete buckets (hourly).
     *
     * @param array $points list of ['label','value','tip'].
     * @param array $opts   optional: 'height' (int), 'unit' (string label).
     * @return string SVG markup.
     */
    public static function bars( array $points, array $opts = array() ) {
        $points = array_values( $points );
        $n      = count( $points );
        if ( 0 === $n ) {
            return '';
        }
        $geo    = self::geometry( $points, $opts );
        $plot_w = $geo['plot_w'];
        $plot_h = $geo['plot_h'];
        $max    = $geo['max'];
        $base_y = self::PAD_T + $plot_h;

        $slot   = $plot_w / $n;
        $bw     = min( $slot * 0.62, 26 ); // cap width so few buckets don't look chunky
        $bars   = '';
        foreach ( $points as $i => $p ) {
            $val = max( 0, (float) $p['value'] );
            $bh  = $val > 0 ? max( 2, $plot_h * ( $val / $max ) ) : 0;
            $x   = self::PAD_L + $i * $slot + ( $slot - $bw ) / 2;
            $y   = $base_y - $bh;
            if ( $bh <= 0 ) {
                continue;
            }
            $bars .= sprintf(
                '<rect class="sf-chart-bar" x="%s" y="%s" width="%s" height="%s" rx="3"><title>%s</title></rect>',
                self::num( $x ),
                self::num( $y ),
                self::num( $bw ),
                self::num( $bh ),
                esc_html( self::tip( $p ) )
            );
        }

        $svg  = self::open_svg( $opts, 'sf-chart-bars' );
        $svg .= self::grid_and_axis( $geo );
        $svg .= $bars;
        $svg .= '</svg>';
        return $svg;
    }

    // ---- internals -----------------------------------------------------

    /** Compute shared geometry (max, plot box, nice y ticks, x label stride). */
    private static function geometry( array $points, array $opts ) {
        $h      = isset( $opts['height'] ) ? max( 140, (int) $opts['height'] ) : self::H;
        $plot_w = self::W - self::PAD_L - self::PAD_R;
        $plot_h = $h - self::PAD_T - self::PAD_B;
        $raw    = 1;
        foreach ( $points as $p ) {
            $raw = max( $raw, (float) $p['value'] );
        }
        $max    = self::nice_ceil( $raw );
        // X-axis label stride: aim for ~7 labels max so they never collide.
        $n      = count( $points );
        $stride = max( 1, (int) ceil( $n / 7 ) );
        return array(
            'h'      => $h,
            'plot_w' => $plot_w,
            'plot_h' => $plot_h,
            'max'    => $max,
            'stride' => $stride,
            'points' => $points,
        );
    }

    /** Opening <svg> tag with a responsive viewBox. */
    private static function open_svg( array $opts, $class ) {
        $h = isset( $opts['height'] ) ? max( 140, (int) $opts['height'] ) : self::H;
        return sprintf(
            '<svg class="sf-chart %s" viewBox="0 0 %d %d" preserveAspectRatio="xMidYMid meet" role="img" xmlns="http://www.w3.org/2000/svg">',
            esc_attr( $class ),
            self::W,
            $h
        );
    }

    /** Horizontal gridlines + y-axis value labels + x-axis labels. */
    private static function grid_and_axis( array $geo ) {
        $plot_w = $geo['plot_w'];
        $plot_h = $geo['plot_h'];
        $max    = $geo['max'];
        $points = $geo['points'];
        $stride = $geo['stride'];
        $right  = self::PAD_L + $plot_w;
        $out    = '';

        // 4 gridlines (0, ¼, ½, ¾, full) with value labels on the left.
        $ticks = 4;
        for ( $t = 0; $t <= $ticks; $t++ ) {
            $frac = $t / $ticks;
            $y    = self::PAD_T + $plot_h * ( 1 - $frac );
            $val  = (int) round( $max * $frac );
            $out .= sprintf(
                '<line class="sf-chart-grid%s" x1="%s" y1="%s" x2="%s" y2="%s"/>',
                0 === $t ? ' is-base' : '',
                self::num( self::PAD_L ),
                self::num( $y ),
                self::num( $right ),
                self::num( $y )
            );
            $out .= sprintf(
                '<text class="sf-chart-ylabel" x="%s" y="%s">%s</text>',
                self::num( self::PAD_L - 8 ),
                self::num( $y + 3 ),
                esc_html( self::short_num( $val ) )
            );
        }

        // X-axis labels (subset).
        $n    = count( $points );
        $step = $n > 1 ? $plot_w / ( $n - 1 ) : 0;
        $slot = $n > 0 ? $plot_w / $n : 0;
        foreach ( $points as $i => $p ) {
            if ( 0 !== $i % $stride && $i !== $n - 1 ) {
                continue;
            }
            // Area points sit on the line; bar labels sit under the slot centre.
            $x = $n > 1 ? self::PAD_L + $i * $step : self::PAD_L + $plot_w / 2;
            $out .= sprintf(
                '<text class="sf-chart-xlabel" x="%s" y="%s">%s</text>',
                self::num( $x ),
                self::num( self::PAD_T + $plot_h + 18 ),
                esc_html( (string) $p['label'] )
            );
        }
        return $out;
    }

    /** Round a max value up to a clean axis bound (1,1.5,2,3,4,5,7.5,10 × 10ⁿ). */
    private static function nice_ceil( $v ) {
        $v = (float) $v;
        if ( $v <= 0 ) {
            return 1;
        }
        $exp  = floor( log10( $v ) );
        $base = pow( 10, $exp );
        $f    = $v / $base;
        if ( $f <= 1 )       { $nf = 1; }
        elseif ( $f <= 1.5 ) { $nf = 1.5; }
        elseif ( $f <= 2 )   { $nf = 2; }
        elseif ( $f <= 3 )   { $nf = 3; }
        elseif ( $f <= 4 )   { $nf = 4; }
        elseif ( $f <= 5 )   { $nf = 5; }
        elseif ( $f <= 7.5 ) { $nf = 7.5; }
        else                 { $nf = 10; }
        return (int) ceil( $nf * $base );
    }

    /** Compact axis numbers: 1200 -> 1.2k. */
    private static function short_num( $n ) {
        $n = (int) $n;
        if ( $n >= 1000 ) {
            $k = $n / 1000;
            return ( floor( $k ) === $k ? (string) (int) $k : number_format_i18n( $k, 1 ) ) . 'k';
        }
        return (string) $n;
    }

    /** Tooltip text for a point. */
    private static function tip( array $p ) {
        if ( ! empty( $p['tip'] ) ) {
            return (string) $p['tip'];
        }
        return (string) $p['label'] . ': ' . number_format_i18n( (float) $p['value'] );
    }

    /** Trim float coordinates to keep the SVG markup small. */
    private static function num( $v ) {
        return rtrim( rtrim( number_format( (float) $v, 2, '.', '' ), '0' ), '.' );
    }

    /* ====================================================================
     * Eigenvergleich widgets (WP4b): a delta badge (icon + text, status
     * colour reserved and never the only signal) and a small inline-SVG
     * sparkline. Both take the plain arrays Seoforge_History_Widgets
     * computes and turn them into escaped, ready-to-echo markup.
     * ==================================================================== */

    /**
     * A "+12% ▲" / "−17pp ▼" style badge. Sentiment (good/bad/neutral) is a
     * CSS class AND ships with an arrow icon + a signed number — never colour
     * alone, so it stays readable without colour vision.
     *
     * @param array $delta Seoforge_History_Widgets::format_delta() result.
     * @param array $opts  'suffix' (string, e.g. '%' or 'pp'), 'no_data_text'.
     * @return string HTML, already escaped.
     */
    public static function delta_badge( array $delta, array $opts = array() ) {
        $suffix = isset( $opts['suffix'] ) ? (string) $opts['suffix'] : '';

        if ( empty( $delta['has_data'] ) ) {
            $no_data = isset( $opts['no_data_text'] )
                ? (string) $opts['no_data_text']
                : __( 'not enough history yet', 'brainwerk-seo-suite' );
            return '<span class="seoforge-delta seoforge-delta-neutral">' . esc_html( $no_data ) . '</span>';
        }

        $icons     = array( 'up' => '▲', 'down' => '▼', 'flat' => '–' );
        $icon      = isset( $icons[ $delta['direction'] ] ) ? $icons[ $delta['direction'] ] : '';
        $sentiment = isset( $delta['sentiment'] ) ? $delta['sentiment'] : 'neutral';
        $num       = (float) $delta['delta'];
        $sign      = $num > 0 ? '+' : ( $num < 0 ? '−' : '' ); // real minus, not a hyphen
        $text      = $sign . number_format_i18n( abs( $num ), abs( $num ) < 10 ? 1 : 0 ) . $suffix;

        return sprintf(
            '<span class="seoforge-delta seoforge-delta-%1$s"><span class="seoforge-delta-icon" aria-hidden="true">%2$s</span>%3$s</span>',
            esc_attr( $sentiment ),
            esc_html( $icon ),
            esc_html( $text )
        );
    }

    /**
     * A tiny inline-SVG sparkline (label carries identity, not colour — this
     * draws a single, un-legended series). Gap days break the line rather
     * than drawing a false zero (see Seoforge_History_Widgets::sparkline_path()).
     *
     * @param array $values ordered oldest-first, null for a missing day.
     * @param array $opts   'width','height'.
     * @return string SVG markup, or '' when there is no data at all.
     */
    public static function sparkline( array $values, array $opts = array() ) {
        $width  = isset( $opts['width'] ) ? (int) $opts['width'] : 120;
        $height = isset( $opts['height'] ) ? (int) $opts['height'] : 28;
        $geo    = Seoforge_History_Widgets::sparkline_path( $values, $width, $height );
        if ( empty( $geo['has_data'] ) ) {
            return '';
        }

        $svg = sprintf(
            '<svg class="sf-spark" viewBox="0 0 %d %d" preserveAspectRatio="none" role="img" aria-hidden="true">',
            $width,
            $height
        );
        foreach ( $geo['segments'] as $seg ) {
            $svg .= '<path class="sf-spark-line" d="' . esc_attr( $seg ) . '" vector-effect="non-scaling-stroke"/>';
        }
        if ( $geo['last_point'] ) {
            list( $x, $y ) = $geo['last_point'];
            $svg .= sprintf(
                '<circle class="sf-spark-dot" cx="%s" cy="%s" r="2"/>',
                self::num( $x ),
                self::num( $y )
            );
        }
        $svg .= '</svg>';
        return $svg;
    }
}
