<?php

namespace Kirby\Filesystem;

use Kirby\Cms\Language;
use Kirby\Toolkit\Str;
use Stringable;

/**
 * The `Filename` class handles complex
 * mapping of file attributes (i.e for thumbnails)
 * into human readable filenames.
 *
 * ```php
 * $filename = new Filename('some-file.jpg', '{{ name }}-{{ attributes }}.{{ extension }}', [
 *   'crop'    => 'top left',
 *   'width'   => 300,
 *   'height'  => 200
 *   'quality' => 80
 * ]);
 *
 * echo $filename->toString();
 * // result: some-file-300x200-crop-top-left-q80.jpg
 *
 * @package   Kirby Filesystem
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      https://getkirby.com
 * @copyright Bastian Allgeier
 * @license   https://getkirby.com/license
 */
class Filename implements Stringable
{
	/**
	 * The sanitized file extension
	 */
	protected string $extension;

	/**
	 * The sanitized file name
	 */
	protected string $name;

	/**
	 * Creates a new Filename object
	 *
	 * @param string $template for the final name
	 * @param array $attributes List of all applicable attributes
	 */
	public function __construct(
		protected string $filename,
		protected string $template,
		protected array $attributes = [],
		protected string|null $language = null
	) {
		$this->name      = $this->sanitizeName($filename);
		$this->extension = $this->sanitizeExtension(
			$attributes['format'] ??
			pathinfo($filename, PATHINFO_EXTENSION)
		);
	}

	/**
	 * Converts the entire object to a string
	 */
	public function __toString(): string
	{
		return $this->toString();
	}

	/**
	 * Converts all processed attributes
	 * to an array. The array keys are already
	 * the shortened versions for the filename
	 */
	public function attributesToArray(): array
	{
		$array = [
			'dimensions' => implode('x', $this->dimensions()),
			'crop'       => $this->crop(),
			'blur'       => $this->blur(),
			'bw'         => $this->grayscale(),
			'q'          => $this->quality(),
			'sharpen'    => $this->sharpen(),
		];

		$array = array_filter(
			$array,
			fn ($item) => $item !== null && $item !== false && $item !== ''
		);

		return $array;
	}

	/**
	 * Converts all processed attributes
	 * to a string, that can be used in the
	 * new filename
	 *
	 * @param string|null $prefix The prefix will be used in the filename creation
	 */
	public function attributesToString(string|null $prefix = null): string
	{
		$array  = $this->attributesToArray();
		$result = [];

		foreach ($array as $key => $value) {
			if ($value === true) {
				$value = '';
			}

			$result[] = match ($key) {
				'dimensions' => $value,
				'crop'       => match ($value) {
					'center' => 'crop',
					default  => $key . '-' . $value
				},
				default      => $key . $value
			};
		}

		$result     = array_filter($result);
		$attributes = implode('-', $result);

		if (empty($attributes) === true) {
			return '';
		}

		return $prefix . $attributes;
	}

	/**
	 * Normalizes the blur option value
	 */
	public function blur(): int|false
	{
		$value = $this->attributes['blur'] ?? false;

		if ($value === false) {
			return false;
		}

		return (int)$value;
	}

	/**
	 * Normalizes the crop option value
	 */
	public function crop(): string|false
	{
		// get the crop value
		$crop = $this->attributes['crop'] ?? false;

		if ($crop === false) {
			return false;
		}

		return Str::slug($crop);
	}

	/**
	 * Returns a normalized array
	 * with width and height values
	 * if available
	 */
	public function dimensions(): array
	{
		if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) {
			return [];
		}

		return [
			'width'  => $this->attributes['width']  ?? null,
			'height' => $this->attributes['height'] ?? null
		];
	}

	/**
	 * Returns the sanitized extension
	 */
	public function extension(): string
	{
		return $this->extension;
	}

	/**
	 * Normalizes the grayscale option value
	 * and also the available ways to write
	 * the option. You can use `grayscale`,
	 * `greyscale` or simply `bw`. The function
	 * will always return `grayscale`
	 */
	public function grayscale(): bool
	{
		// normalize options
		$value =
			$this->attributes['grayscale'] ??
			$this->attributes['greyscale'] ??
			$this->attributes['bw'] ??
			false;

		// turn anything into boolean
		return filter_var($value, FILTER_VALIDATE_BOOLEAN);
	}

	/**
	 * Returns the filename without extension
	 */
	public function name(): string
	{
		return $this->name;
	}

	/**
	 * Normalizes the quality option value
	 */
	public function quality(): int|false
	{
		$value = $this->attributes['quality'] ?? false;

		if ($value === false || $value === true) {
			return false;
		}

		return (int)$value;
	}

	/**
	 * Sanitizes the file extension.
	 * It also replaces `jpeg` with `jpg`.
	 */
	protected function sanitizeExtension(string $extension): string
	{
		$extension = F::safeExtension('test.' . $extension);
		$extension = str_replace('jpeg', 'jpg', $extension);
		return $extension;
	}

	/**
	 * Sanitizes the file name
	 */
	protected function sanitizeName(string $name): string
	{
		// temporarily store language rules
		$rules = Str::$language;

		// add rules for a particular language to `Str` class
		if ($this->language !== null) {
			Str::$language = [
				...Str::$language,
				...Language::loadRules($this->language)];
		}

		try {
			return F::safeBasename($this->filename);
		} finally {
			// restore language rules even if F::safeBasename throws
			Str::$language = $rules;
		}
	}

	/**
	 * Normalizes the sharpen option value
	 */
	public function sharpen(): int|false
	{
		return match ($this->attributes['sharpen'] ?? false) {
			false   => false,
			true    => 50,
			default => (int)$this->attributes['sharpen']
		};
	}

	/**
	 * Returns the converted filename as string
	 */
	public function toString(): string
	{
		return Str::template($this->template, [
			'name'       => $this->name(),
			'attributes' => $this->attributesToString('-'),
			'extension'  => $this->extension()
		], ['fallback' => '']);
	}
}
