<?php

use Imagine\Image\Box;
use Imagine\Image\Point;
use MillionDollarScript\Classes\Data\Options;
use MillionDollarScript\Classes\System\Logs;
use MillionDollarScript\Classes\System\Utility;

/*
 * Million Dollar Script Two
 *
 * @author      Ryan Rhode
 * @copyright   (C) 2025, Ryan Rhode
 * @license     https://opensource.org/licenses/GPL-3.0 GNU General Public License, version 3
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 *    Million Dollar Script
 *    Pixels to Profit: Ignite Your Revolution
 *    https://milliondollarscript.com/
 *
 */

defined( 'ABSPATH' ) or exit;

/**
 * Normalize Imagine save() options to ensure Imagick receives a valid integer 'quality'.
 *
 * Rules:
 * - Always return an array.
 * - For JPEG (jpg/jpeg):
 *     - Compute $q from 'jpeg_quality' if numeric, else from 'quality' if numeric, else default 90.
 *     - Set both 'jpeg_quality' and 'quality' to the integer $q.
 * - For PNG:
 *     - Preserve 'png_compression_level' if present.
 *     - Still provide integer 'quality' (Imagick may call setImageCompressionQuality).
 * - For GIF or others:
 *     - Preserve existing flags (e.g., 'flatten' => false) but ensure integer 'quality'.
 *
 * @param string $ext File extension without dot, e.g. 'jpg','png','gif'
 * @param array|null $options Options passed to Imagine->save/show
 * @param int $default Default quality to use when unspecified or invalid
 * @return array Normalized options
 */
function mds_normalize_imagine_save_options(string $ext, ?array $options, int $default = 90): array {
    $opts = is_array($options) ? $options : [];

    $normalizedExt = strtolower($ext);
    if ($normalizedExt === 'jpeg') {
        $normalizedExt = 'jpg';
    }

    if ($normalizedExt === 'jpg') {
        $q = $default;
        if (isset($opts['jpeg_quality']) && is_numeric($opts['jpeg_quality'])) {
            $q = (int)$opts['jpeg_quality'];
        } elseif (isset($opts['quality']) && is_numeric($opts['quality'])) {
            $q = (int)$opts['quality'];
        }
        $opts['jpeg_quality'] = $q;
        $opts['quality'] = $q;
    } else {
        // PNG/GIF/others: ensure a sane integer 'quality'
        if (!isset($opts['quality']) || !is_numeric($opts['quality'])) {
            $opts['quality'] = $default;
        } else {
            $opts['quality'] = (int)$opts['quality'];
        }
        // Leave format-specific keys intact (e.g., 'png_compression_level', 'flatten')
    }

    return $opts;
}

/**
 * Applies opacity to a GD image resource by modifying its alpha channel.
 *
 * @param \GdImage $image The image resource to modify.
 * @param int $opacity Opacity percentage (0-100).
 * @return bool True on success, false on failure.
 */
function mds_apply_opacity_gd(\GdImage &$image, int $opacity): bool {

    // Check for invalid opacity values
    if ($opacity < 0 || $opacity > 100) {
        return false; 
    }

    // No change needed if opacity is 100%
    if ($opacity == 100) {
        return true; 
    }

    // Convert indexed to true color if necessary
    if (!imageistruecolor($image)) {
        imagetruecolortopalette($image, false, 256); 
    }

    $width = imagesx($image);
    $height = imagesy($image);

    // Disable alpha blending during pixel manipulation
    imagealphablending($image, false); 

    // Ensure alpha channel is saved when the image is output
    imagesavealpha($image, true);

    $opacityFactor = $opacity / 100.0;

    for ($x = 0; $x < $width; $x++) {
        for ($y = 0; $y < $height; $y++) {
            $rgba = imagecolorat($image, $x, $y);

            // Extract existing alpha (0=opaque, 127=transparent)
            $alpha = ($rgba & 0x7F000000) >> 24; 

            // Calculate new alpha based on opacity factor
            // Formula: New = 127 - ( (127 - Old) * Factor )
            $newAlpha = 127 - (int)(((127 - $alpha) * $opacityFactor)); 

            // Clamp newAlpha to GD's valid range (0-127)
            $newAlpha = max(0, min(127, $newAlpha));

            // Allocate the new color with the calculated alpha
            $color = imagecolorallocatealpha($image, 
                ($rgba & 0xFF0000) >> 16,
                ($rgba & 0x00FF00) >> 8,
                $rgba & 0x0000FF,
                $newAlpha
            );

            // Check for color allocation failure
            if ($color === false) return false; 

            // Set the pixel to the new color
            imagesetpixel($image, $x, $y, $color);
        }
    }

    // Re-enable alpha blending if it might be needed later
    imagealphablending($image, true);

    return true;
}

/** Resolve Blocks Layering mode for a grid. */
function mds_get_grid_blocks_layering(int $BID): string {
    $val = get_option( 'mds_grid_blocks_layering_' . $BID, 'below' );
    return ($val === 'above') ? 'above' : 'below';
}

/** Resolve overlay block image absolute path for a grid/context/mode. */
function mds_resolve_overlay_path(int $BID, string $variant): string {
    // $variant: 'public-light', 'public-dark', 'ordering-light', 'ordering-dark'
    $opt = get_option( 'mds_grid_overlay_images_' . $BID, [] );
    $ctx = str_contains($variant, 'ordering') ? 'ordering' : 'public';
    $mode = str_contains($variant, 'dark') ? 'dark' : 'light';

    // Uploaded file (stored as basename)
    $basename = '';
    if (is_array($opt) && isset($opt[$ctx]) && is_array($opt[$ctx]) && !empty($opt[$ctx][$mode])) {
        $basename = $opt[$ctx][$mode];
    }
    if (!empty($basename)) {
        $upload_dir = Utility::get_upload_path() . 'grids/overlays/';
        $path = wp_normalize_path( $upload_dir . $basename );
        if ( file_exists( $path ) ) {
            return $path;
        }
    }

    // Fallback to bundled defaults
    $images_dir = wp_normalize_path( MDS_CORE_PATH . 'images/' );
    $fallback = $images_dir . 'overlay-' . $variant . '.png';
    return $fallback;
}

function mds_grid_render_estimate( array $banner_data ): array {
	$cols       = max( 0, intval( $banner_data['G_WIDTH'] ?? $banner_data['grid_width'] ?? 0 ) );
	$rows       = max( 0, intval( $banner_data['G_HEIGHT'] ?? $banner_data['grid_height'] ?? 0 ) );
	$blkW       = max( 0, intval( $banner_data['BLK_WIDTH'] ?? $banner_data['block_width'] ?? 0 ) );
	$blkH       = max( 0, intval( $banner_data['BLK_HEIGHT'] ?? $banner_data['block_height'] ?? 0 ) );
	$cells      = $cols * $rows;
	$gridW      = $cols * $blkW;
	$gridH      = $rows * $blkH;
	$imageBytes = $gridW * $gridH * 6;
	$cellBytes  = $cells * 96;
	$estimated  = $imageBytes + $cellBytes + ( 16 * 1024 * 1024 );
	$limit      = function_exists( 'wp_convert_hr_to_bytes' ) ? wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) ) : 0;
	$warn       = $cells >= 250000 || ( $limit > 0 && $estimated > ( $limit * 0.70 ) );

	return [
		'columns'         => $cols,
		'rows'            => $rows,
		'cells'           => $cells,
		'width'           => $gridW,
		'height'          => $gridH,
		'estimated_bytes' => $estimated,
		'memory_limit'    => $limit,
		'warn'            => $warn,
	];
}

function mds_format_bytes( int $bytes ): string {
	if ( function_exists( 'size_format' ) ) {
		return size_format( $bytes );
	}

	if ( $bytes >= 1048576 ) {
		return round( $bytes / 1048576, 1 ) . ' MB';
	}

	if ( $bytes >= 1024 ) {
		return round( $bytes / 1024, 1 ) . ' KB';
	}

	return $bytes . ' bytes';
}

function mds_paste_grid_block( $map, $img, bool $useGd, $dstRes, int $x, int $y, int $blkW, int $blkH ): void {
	if ( $useGd && $img instanceof Imagine\Gd\Image ) {
		$src = $img->getGdResource();
		imagecopy( $dstRes, $src, $x, $y, 0, 0, $blkW, $blkH );
		return;
	}

	$map->paste( $img, new Imagine\Image\Point( $x, $y ) );
}

/**
 * Output grid map
 *
 * @param bool $show Show the grid if true, save to file if false.
 * @param string $file File to save the grid to if $show is false.
 * @param int $BID The grid id to use.
 * @param array $types An array of types of blocks to show.
 *                      array(
 *                          'background',
 *                          'orders',
 *                          'grid',
 *                          'nfs',
 *                          'nfs_front',
 *                          'ordered',
 *                          'reserved',
 *                          'selected',
 *                          'sold',
 *                          'price_zones',
 *                          'price_zones_text',
 *                          'user',
 *                          'not_my_reserved'
 *                       )
 *
 * @param int $user_id
 *
 * @return string Progress output.
 */
function output_grid( $show, $file, $BID, $types, $user_id = 0, $cached = false, $ordering = false ) {
	global $wpdb;
	
	if ( ! is_numeric( $BID ) ) {
		return false;
	}

	// TODO: add shutdown detection for when this times out
	// TODO: add cleanup of old images

	if ( $cached ) {

		// Banner paths
		$BANNER_PATH = \MillionDollarScript\Classes\System\Utility::get_upload_path() . 'grids/';

		// Get the file extension if the file doesn't already have one
		$ext = pathinfo( $file, PATHINFO_EXTENSION );
		if ( empty( $ext ) ) {
			$ext = \MillionDollarScript\Classes\System\Utility::get_file_extension();
		}

		// Get the last modification time of the orders
		$last_modification_time = \MillionDollarScript\Classes\Orders\Orders::get_last_order_modification_time();

		// Get the hash value of the types of blocks to show in the grid
		$typehash = md5( serialize( $types ) );

		// Get the file path and url
		$filename = "grid$BID-$last_modification_time-$typehash";
		$file     = $BANNER_PATH . $filename;
		$fullfile = wp_normalize_path( $file . '.' . $ext );

		// grid1-856848500-1bf8f4d8717252cf8010a67f0b3c50b0.png
		if ( file_exists( $fullfile ) && filemtime( $fullfile ) >= $last_modification_time ) {
			// If the file exists then output it directly.
			header( 'Content-Type:' . 'image/' . $ext );
			header( 'Content-Length: ' . filesize( $fullfile ) );
			readfile( $fullfile );
		} else {
			// The file doesn't exist so save it and then output it.
			// save the grid image since it doesn't exist yet
			output_grid( false, $file, $BID, $types, $user_id, false, $ordering );
			output_grid( false, $file, $BID, $types, $user_id, true, $ordering );
		}

		return "Saved " . $fullfile;
	}

	$progress = 'Please wait.. Processing the Grid image with GD';

	// Dynamically select image driver: Imagick if available, otherwise GD
	if ( extension_loaded('imagick') && class_exists('Imagick') ) {
		$imagine = new Imagine\Imagick\Imagine();
	} elseif ( function_exists('gd_info') ) {
		$imagine = new Imagine\Gd\Imagine();
	} else {
		wp_die('No supported image driver available.');
	}

	// Determine if using GD driver
	$useGd = ($imagine instanceof \Imagine\Gd\Imagine);

	$banner_data = load_banner_constants( $BID );

	// Determine grid layering mode
	$mds_blocks_layering = mds_get_grid_blocks_layering( (int)$BID );

	// Precompute grid dimensions
	$blkW = (int)$banner_data['BLK_WIDTH'];
	$blkH = (int)$banner_data['BLK_HEIGHT'];
	$cols = (int)$banner_data['G_WIDTH'];
	$rows = (int)$banner_data['G_HEIGHT'];
	$gridW = $cols * $blkW;
	$gridH = $rows * $blkH;
	$render_estimate = mds_grid_render_estimate( $banner_data );
	if ( ! $show && ! empty( $render_estimate['warn'] ) ) {
		Logs::log(
			'MDS grid render estimate warning for BID ' . intval( $BID ) . ': ' .
			intval( $render_estimate['cells'] ) . ' cells, ' .
			intval( $render_estimate['width'] ) . 'x' . intval( $render_estimate['height'] ) . 'px, estimated memory ' .
			mds_format_bytes( intval( $render_estimate['estimated_bytes'] ) )
		);
	}

	// load blocks
	$block_size  = new Imagine\Image\Box( $blkW, $blkH );
	$palette     = new Imagine\Image\Palette\RGB();
	$color       = $palette->color( '#000', 0 );
	$zero_point  = new Imagine\Image\Point( 0, 0 );
	$blank_block = $imagine->create( $block_size, $color );

	// default grid block
	/** @var \Imagine\Gd\Image $default_block */
	$default_block = $blank_block->copy();

	if ( ! $ordering ) {
		$image_data = $banner_data['GRID_BLOCK'];
	} else {
		$image_data = $banner_data['USR_GRID_BLOCK'];
	}

	// Check for empty image data before processing
	if ( empty( $image_data ) ) {
		throw new \Exception( 'Grid block image data is empty for Banner ID ' . $BID . '. Please upload a default grid block image in the grid settings.' );
	}

	try {
		// First, try to load with the default Imagine driver (usually Imagick)
		$tmp_block = $imagine->load( $image_data );
	} catch ( \Imagine\Exception\RuntimeException $e ) {
		// Log the Imagick failure
		error_log( '[MDS Fallback] Imagick failed to load image for Banner ID: ' . $BID . '. Error: ' . $e->getMessage() . '. Falling back to GD.' );

		// If Imagick fails, fall back to the GD driver
		$gd_imagine = new \Imagine\Gd\Imagine();
		try {
			$tmp_block = $gd_imagine->load( $image_data );
		} catch ( \Imagine\Exception\RuntimeException $gd_e ) {
			// If GD also fails, log it and re-throw the exception to stop the process
			error_log( '[MDS Fallback] GD also failed to load image for Banner ID: ' . $BID . '. Error: ' . $gd_e->getMessage() );
			throw $gd_e;
		}
	}

	$tmp_block->resize( $block_size );
	$default_block->paste( $tmp_block, $zero_point );

	$and_user = $and_not_user = "";

	foreach ( $types as $type ) {
		switch ( $type ) {
			case 'background':
				// Use glob to find any background image for this grid
				$background_files = glob( \MillionDollarScript\Classes\System\Utility::get_upload_path() . "grids/background{$BID}.*" );
				if ( ! empty($background_files) ) {
					// Open the first found background file
					$background = $imagine->open( $background_files[0] );
				}
				break;
			case 'orders':
				$show_orders       = true;
				$orders_grid_block = $blank_block->copy();
				$tmp_block         = $imagine->load( $banner_data['USR_ORD_BLOCK'] );
				$tmp_block->resize( $block_size );
				$orders_grid_block->paste( $tmp_block, $zero_point );
				break;
			case 'grid':
				// this is the default grid block created above
				$show_grid = true;
				break;
			case 'nfs':
				$default_nfs_block = $blank_block->copy();
				$tmp_block         = $imagine->load( $banner_data['USR_NFS_BLOCK'] );
				$tmp_block->resize( $block_size );
				$default_nfs_block->paste( $tmp_block, $zero_point );
				break;
			case 'nfs_front':
				$default_nfs_front_block = $blank_block->copy();
				$tmp_block               = $imagine->load( $banner_data['NFS_BLOCK'] );
				$tmp_block->resize( $block_size );
				$default_nfs_front_block->paste( $tmp_block, $zero_point );
				break;
			case 'ordered':
				$default_ordered_block = $blank_block->copy();
				$tmp_block             = $imagine->load( $banner_data['USR_ORD_BLOCK'] );
				$tmp_block->resize( $block_size );
				$default_ordered_block->paste( $tmp_block, $zero_point );
				break;
			case 'reserved':
				$default_reserved_block = $blank_block->copy();
				$tmp_block              = $imagine->load( $banner_data['USR_RES_BLOCK'] );
				$tmp_block->resize( $block_size );
				$default_reserved_block->paste( $tmp_block, $zero_point );
				break;
			case 'selected':
				$default_selected_block = $blank_block->copy();
				$tmp_block              = $imagine->load( $banner_data['USR_SEL_BLOCK'] );
				$tmp_block->resize( $block_size );
				$default_selected_block->paste( $tmp_block, $zero_point );
				break;
			case 'sold':
				$default_sold_block = $blank_block->copy();
				$tmp_block          = $imagine->load( $banner_data['USR_SOL_BLOCK'] );
				$tmp_block->resize( $block_size );
				$default_sold_block->paste( $tmp_block, $zero_point );
				break;
			case 'price_zones':
				$show_price_zones = true;

				$cyan       = $palette->color( array( 0, 255, 255 ), 50 );
				$cyan_block = $imagine->create( $block_size, $cyan );

				$yellow       = $palette->color( array( 255, 255, 0 ), 50 );
				$yellow_block = $imagine->create( $block_size, $yellow );

				$magenta       = $palette->color( array( 255, 0, 255 ), 50 );
				$magenta_block = $imagine->create( $block_size, $magenta );

				$white       = $palette->color( array( 255, 255, 255 ), 50 );
				$white_block = $imagine->create( $block_size, $white );
				break;
			case 'price_zones_text':
				$show_price_zones_text = true;
				break;
			case 'user':
				$user     = $user_id;
				$and_user = " AND `user_id`=" . intval( $user );
				break;
			case 'not_my_reserved':
				$and_not_user = " AND `user_id`!=" . get_current_user_id();
				break;
			default:
				break;
		}
	}

	$blocks = $orders = $users = $nfs = array();

	// preload nfs blocks
	if ( isset( $default_nfs_block ) ) {
		global $wpdb;
		$sql = $wpdb->prepare(
			"SELECT block_id FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='nfs' AND banner_id=%d" . $and_user,
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		// nfs covered images
		if ( isset( $banner_data['NFS_COVERED'] ) == 'Y' ) {
			$sql = $wpdb->prepare(
				"SELECT block_id,image_data FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='nfs' AND image_data <> '' AND banner_id=%d",
				intval( $BID )
			);
			$result = $wpdb->get_results( $sql );

			foreach ( $result as $row ) {

				$data = $row->image_data;

				// get block image output
				if ( strlen( $data ) != 0 ) {
					$block = $imagine->load( base64_decode( $data ) );
				} else {
					$block = $default_nfs_block->copy();
				}
				$block->resize( $block_size );

				$blocks[ $row->block_id ] = 'nfs';
				$nfs[ $row->block_id ]    = $block;
			}
		} else {
			foreach ( $result as $row ) {
				$blocks[ $row->block_id ] = 'nfs';
				$nfs[ $row->block_id ]    = $default_nfs_block;
			}
		}
	}

	// preload nfs_front blocks (nfs blocks appearing in front of the background)
	if ( isset( $default_nfs_front_block ) ) {
		$sql = $wpdb->prepare(
			"SELECT block_id FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='nfs' AND banner_id=%d" . $and_user,
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		// nfs covered images
		if ( isset( $banner_data['NFS_COVERED'] ) && $banner_data['NFS_COVERED'] == 'Y' ) {
			$sql = $wpdb->prepare(
				"SELECT block_id,image_data FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='nfs' AND image_data <> '' AND banner_id=%d",
				intval( $BID )
			);
			$result = $wpdb->get_results( $sql );

			foreach ( $result as $row ) {

				$data = $row->image_data;

				// get block image output
				if ( strlen( $data ) != 0 ) {
					$block = $imagine->load( base64_decode( $data ) );
				} else {
					$block = $default_nfs_front_block->copy();
				}
				$block->resize( $block_size );

				$blocks[ $row->block_id ] = 'nfs_front';
				$nfs[ $row->block_id ]    = $block;
			}
		} else {
			foreach ( $result as $row ) {
				$blocks[ $row->block_id ] = 'nfs_front';
				$nfs[ $row->block_id ]    = $default_nfs_front_block;
			}
		}
	}

	// preload ordered blocks
	if ( isset( $default_ordered_block ) ) {
		$sql = $wpdb->prepare(
			"SELECT block_id FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='ordered' AND banner_id=%d" . $and_user,
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		foreach ( $result as $row ) {
			$blocks[ $row->block_id ] = 'ordered';
		}
	}

	// preload reserved blocks (include cancelled to visually block them until cleanup)
	if ( isset( $default_reserved_block ) ) {
		$current_user_id = get_current_user_id();
		$sql = $wpdb->prepare(
			"SELECT block_id FROM " . MDS_DB_PREFIX . "blocks 
			 WHERE banner_id=%d 
			   AND ( (status = 'reserved' AND user_id != %d) OR status = 'cancelled')",
			intval( $BID ),
			intval( $current_user_id )
		);
		$result = $wpdb->get_results( $sql );

		foreach ( $result as $row ) {
			$blocks[ $row->block_id ] = 'reserved';
		}
	}

	// preload selected blocks
	if ( isset( $default_selected_block ) ) {
		$sql = $wpdb->prepare(
			"SELECT block_id FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='onorder' AND banner_id=%d" . $and_user,
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		foreach ( $result as $row ) {
			$blocks[ $row->block_id ] = 'selected';
		}
	}

	// preload sold blocks
	if ( isset( $default_sold_block ) ) {
		$sql = $wpdb->prepare(
			"SELECT block_id FROM " . MDS_DB_PREFIX . "blocks WHERE `status`='sold' AND banner_id=%d" . $and_user,
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		foreach ( $result as $row ) {
			$blocks[ $row->block_id ] = 'sold';
		}
	}

	// preload orders
	if ( isset( $show_orders ) ) {
		$sql = $wpdb->prepare(
			"SELECT block_id,image_data FROM " . MDS_DB_PREFIX . "blocks WHERE approved='Y' AND `status`='sold' AND image_data <> '' AND banner_id=%d" . $and_user,
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		foreach ( $result as $row ) {

			$data = $row->image_data;

			// get block image output
			if ( strlen( $data ) != 0 ) {
				$block = $imagine->load( base64_decode( $data ) );
			} else {
				$block = $orders_grid_block->copy();
			}
			// Conditionally resize based on auto-resize option
			if (Options::get_option('resize') == 'YES') {
				// Force the image to fill the entire block space
				$block = $block->thumbnail($block_size, Imagine\Image\ManipulatorInterface::THUMBNAIL_OUTBOUND);
				// Ensure the processed image is exactly the block size
				if ($block->getSize()->getWidth() !== $blkW || $block->getSize()->getHeight() !== $blkH) {
					$blank = $imagine->create($block_size);
					$blank->paste($block, new Point(0, 0));
					$block = $blank;
				}
			}
			$blocks[ $row->block_id ] = 'order';
			$orders[ $row->block_id ] = $block;
		}
	}

	// Show user's blocks
	if ( isset( $user ) ) {
		$sql = $wpdb->prepare(
			"SELECT block_id,image_data FROM " . MDS_DB_PREFIX . "blocks WHERE `user_id`=%d AND image_data <> '' AND banner_id=%d AND `status` NOT IN ('deleted', 'cancelled')",
			intval( $user ),
			intval( $BID )
		);
		$result = $wpdb->get_results( $sql );

		foreach ( $result as $row ) {

			$data = $row->image_data;

			// get block image output
			if ( strlen( $data ) != 0 ) {
				$block = $imagine->load( base64_decode( $data ) );
			} else {
				$block = $orders_grid_block->copy();
			}
			// Conditionally resize based on auto-resize option
			if (Options::get_option('resize') == 'YES') {
				// Force the image to fill the entire block space
				$block = $block->thumbnail($block_size, Imagine\Image\ManipulatorInterface::THUMBNAIL_OUTBOUND);
				// Ensure the processed image is exactly the block size
				$blank = $imagine->create($block_size);
				$blank->paste($block, new Point(0, 0));
				$block = $blank;
			}
			$blocks[ $row->block_id ] = 'user';
			$users[ $row->block_id ]  = $block;
		}
	}

	// grid size
	$size = new Imagine\Image\Box( $gridW, $gridH );

	// create base canvas (fill color when Above mode)
	if ( $mds_blocks_layering === 'above' ) {
		$bg_colors = get_option( 'mds_grid_bg_colors_' . $BID, array('light' => '#ffffff', 'dark' => '#14181c') );
		$theme_mode = ( Options::get_option( 'theme_mode', 'light' ) === 'dark' ) ? 'dark' : 'light';
		$base_hex = $bg_colors[$theme_mode] ?? ( $theme_mode === 'dark' ? '#14181c' : '#ffffff' );
		$map = $imagine->create( $size, $palette->color( $base_hex, 100 ) );
	} else {
		$map = $imagine->create( $size );
	}
	/** @var \Imagine\Gd\Image $map */
	// detect GD driver for direct imagecopy
	$useGd = $map instanceof Imagine\Gd\Image;
	if ( $useGd ) {
		/** @var \GdImage $dstRes */
		$dstRes = $map->getGdResource();
	}

	$front_blocks = [];

	// Grid and NFS blocks go behind the background. Front layers are collected only for occupied cells.
	$cell = 0;
	for ( $row = 0, $y = 0; $row < $rows; $row++, $y += $blkH ) {
		for ( $col = 0, $x = 0; $col < $cols; $col++, $x += $blkW ) {
			$status     = $blocks[ $cell ] ?? '';
			$back_block = null;

			if ( '' !== $status ) {
				if ( isset( $show_orders ) && $status === 'order' && isset( $orders[ $cell ] ) ) {
					$front_blocks[] = [ $x, $y, $orders[ $cell ] ];
				} else if ( isset( $user ) && $status === 'user' && isset( $users[ $cell ] ) ) {
					$front_blocks[] = [ $x, $y, $users[ $cell ] ];
				} else if ( isset( $default_nfs_block ) && $status === 'nfs' && isset( $nfs[ $cell ] ) ) {
					$back_block = $nfs[ $cell ];
				} else if ( isset( $default_nfs_front_block ) && $status === 'nfs_front' && isset( $nfs[ $cell ] ) ) {
					$front_blocks[] = [ $x, $y, $nfs[ $cell ] ];
				} else if ( isset( $default_ordered_block ) && $status === 'ordered' ) {
					$front_blocks[] = [ $x, $y, $default_ordered_block ];
				} else if ( isset( $default_reserved_block ) && $status === 'reserved' ) {
					$front_blocks[] = [ $x, $y, $default_reserved_block ];
				} else if ( isset( $default_selected_block ) && $status === 'selected' ) {
					$front_blocks[] = [ $x, $y, $default_selected_block ];
				} else if ( isset( $default_sold_block ) && $status === 'sold' ) {
					$front_blocks[] = [ $x, $y, $default_sold_block ];
				}
			}

			if ( null === $back_block && isset( $show_grid ) && $mds_blocks_layering !== 'above' ) {
				$back_block = $default_block;
			} else if ( null === $back_block && ! isset( $show_grid ) && $mds_blocks_layering !== 'above' ) {
				$back_block = $blank_block;
			}

			if ( null !== $back_block ) {
				mds_paste_grid_block( $map, $back_block, $useGd, $dstRes ?? null, $x, $y, $blkW, $blkH );
			}

			$cell ++;
		}
	}

	// blend in the background
	if ( isset( $background ) ) {
		// Background image size
		$bgsize = $background->getSize();

		// Rescale image to fit within the size of the grid
		$new_dimensions = resize_dimensions( $gridW, $gridH, $bgsize->getWidth(), $bgsize->getHeight() );

		// Resize to max size
		$background->resize( new Imagine\Image\Box( $new_dimensions['width'], $new_dimensions['height'] ) );

		// Get new size
		$bgsize = $background->getSize();

		// calculate coords to paste at
		$bgx = ( $gridW / 2 ) - ( $bgsize->getWidth() / 2 );
		$bgy = ( $gridH / 2 ) - ( $bgsize->getHeight() / 2 );

		// make sure coords aren't outside the box
		if ( $bgx < 0 ) {
			$bgx = 0;
		}
		if ( $bgy < 0 ) {
			$bgy = 0;
		}

		// Get opacity setting (0-100, default 100)
		$opacity = get_option( 'mds_background_opacity_' . $BID, 100 );

		// paste background image into grid
		if ( $useGd ) {
			/** @var \Imagine\Gd\Image $img */
			$img = $background;
			/** @var \GdImage $src */
			$src = $img->getGdResource();

			// Apply opacity using imagecopymerge for GD driver
			imagealphablending($dstRes, true);
			imagesavealpha($dstRes, true);
			imagecopymerge( $dstRes, $src, $bgx, $bgy, 0, 0, $bgsize->getWidth(), $bgsize->getHeight(), (int)$opacity );

		} else {
			// Apply opacity with Imagick (compatible with Imagick 3.x)
			// Opacity isn't standardized in Imagine core effects, so driver-specific logic is needed.
			/** @var \Imagine\Imagick\Image $background */

			// Get the underlying Imagick object
			/** @var \Imagick $imagick */
			$imagick = $background->getImagick(); 
			// Ensure alpha channel is usable
			$imagick->setImageAlphaChannel(\Imagick::ALPHACHANNEL_ACTIVATE); 
			// Multiply alpha channel by opacity factor
			$imagick->evaluateImage(\Imagick::EVALUATE_MULTIPLY, $opacity / 100.0, \Imagick::CHANNEL_ALPHA);
			$map->paste( $background, new Imagine\Image\Point( $bgx, $bgy ) );
		}
	}

	// When Above, overlay grid-line blocks between background and front blocks
	if ( isset($show_grid) && $mds_blocks_layering === 'above' ) {
		$mode = ( Options::get_option( 'theme_mode', 'light' ) === 'dark' ) ? 'dark' : 'light';
		$variant = ( $ordering ? 'ordering' : 'public' ) . '-' . $mode;
		$overlay_path = mds_resolve_overlay_path( (int)$BID, $variant );
		try {
			$overlay_block = $imagine->open( $overlay_path );
			$overlay_block->resize( $block_size );
			for ( $row = 0, $y = 0; $row < $rows; $row++, $y += $blkH ) {
				for ( $col = 0, $x = 0; $col < $cols; $col++, $x += $blkW ) {
					if ( $useGd ) {
						/** @var \Imagine\Gd\Image $img */
						$img = $overlay_block;
						/** @var \GdImage $src */
						$src = $img->getGdResource();
						imagealphablending($dstRes, true);
						imagesavealpha($dstRes, true);
						imagecopy( $dstRes, $src, $x, $y, 0, 0, $blkW, $blkH );
					} else {
						$map->paste( $overlay_block, new Imagine\Image\Point( $x, $y ) );
					}
				}
			}
		} catch ( \Exception $e ) {
			Logs::log( 'MDS overlay error for BID ' . (int)$BID . ': ' . $e->getMessage() );
		}
	}

	// paste the blocks
	foreach ( $front_blocks as $front_block ) {
		mds_paste_grid_block( $map, $front_block[2], $useGd, $dstRes ?? null, $front_block[0], $front_block[1], $blkW, $blkH );
	}

	// paste price zone layer
	if ( isset( $show_price_zones ) ) {
		for ( $row = 0, $y = 0; $row < $rows; $row++, $y += $blkH ) {
			for ( $col = 0, $x = 0; $col < $cols; $col++, $x += $blkW ) {
				$price_zone_color = get_zone_color( $BID, $y, $x );
				switch ( $price_zone_color ) {
					case "cyan":
						mds_paste_grid_block( $map, $cyan_block, $useGd, $dstRes ?? null, $x, $y, $blkW, $blkH );
						break;
					case "yellow":
						mds_paste_grid_block( $map, $yellow_block, $useGd, $dstRes ?? null, $x, $y, $blkW, $blkH );
						break;
					case "magenta":
						mds_paste_grid_block( $map, $magenta_block, $useGd, $dstRes ?? null, $x, $y, $blkW, $blkH );
						break;
					case "white":
						mds_paste_grid_block( $map, $white_block, $useGd, $dstRes ?? null, $x, $y, $blkW, $blkH );
						break;
					default:
						break;
				}
			}
		}
	}

	// output price zone text
	if ( isset( $show_price_zones_text ) ) {
		/** @var \Imagine\Gd\Image $zmap */
		// skip PNG roundtrip when using GD driver
		if ( $map instanceof Imagine\Gd\Image ) {
			$zmap = $map;
		} else {
			$gdImagine = new Imagine\Gd\Imagine();
			$binary   = $map->get('png', ['png_compression_level' => 9]);
			/** @var \Imagine\Gd\Image $zmap */
			$zmap      = $gdImagine->load($binary);
			unset( $map );
		}
		/** @var \GdImage $gdRes */
		$gdRes = $zmap->getGdResource();

		$row_c       = 0;
		$col_c       = 0;
		$textcolor   = imagecolorallocate( $gdRes, 0, 0, 0 );
		$textcolor_w = imagecolorallocate( $gdRes, 255, 255, 255 );

		for ( $y = 0; $y < $gridH; $y += $blkH ) {
			for ( $x = 0; $x < $gridW; $x += $blkW ) {

				if ( $y == 0 ) {
					$spaces = str_repeat( ' ', 3 - strlen( $col_c ) );
					imagestringup( $gdRes, 1, $x, 15, $spaces . "$col_c", $textcolor_w );
					imagestringup( $gdRes, 1, $x + 1, 15 + 1, $spaces . "$col_c", $textcolor );
					$col_c ++;
				}
			}

			imagestring( $gdRes, 1, 1, $y, "$row_c", $textcolor_w );
			imagestring( $gdRes, 1, 2, $y + 1, "$row_c", $textcolor );
			$row_c ++;
		}

		$map = $zmap;
	}

	// set output options
	$ext     = "png";
	$mime    = "png";
	$options = array( 'png_compression_level' => 9 );

	$OUTPUT_JPEG = Options::get_option( 'output-jpeg' );

	if ( $OUTPUT_JPEG == 'Y' ) {
		$ext     = "jpg";
		$mime    = "jpeg";
		$options = array( 'jpeg_quality' => Options::get_option( 'jpeg-quality' ) );
	} else if ( $OUTPUT_JPEG == 'N' ) {
		// defaults to png, set above
	} else if ( $OUTPUT_JPEG == 'GIF' ) {
		$ext     = "gif";
		$mime    = "gif";
		$options = array( 'flatten' => false );
	}

	// Normalize once centrally
	$options = mds_normalize_imagine_save_options($ext, $options, 90);

	// output
	if ( $show ) {
		if ( Options::get_option( 'interlace-switch' ) == 'YES' ) {
			$map->interlace( Imagine\Image\ImageInterface::INTERLACE_LINE );
		}

		$map->show( $mime, $options );
	} else {

		$pathinfo = pathinfo( $file );
		$filename = wp_normalize_path( $pathinfo['dirname'] . DIRECTORY_SEPARATOR . $pathinfo['filename'] . "." . $ext );
		if ( ! touch( $filename ) ) {
			$progress .= "<b>Warning:</b> The script does not have permission write to " . $filename . " or the directory does not exist<br>";
		}

		// Final guard using the shared helper
		$options = mds_normalize_imagine_save_options($ext, $options, 90);

		$map->save( $filename, $options );
		$progress .= "<br>Saved as " . $filename . "<br>";
	}

	return $progress;
}
