<?php

// TODO: [notImportant] sa pot sa adaug functii pe coloane (si cele std si custom)

// TODO: [notImportant] trateaza coloane disabled - sa nu pot sa le adaug din interfata

// TODO: [notImportant] coloanele disabled ar trb si ele traduse, caci e nevoie de ele sa fie afisate ergonomic la administrare raport (eg. coloanele de

// TODO: [refactorReportSerialization] portare rapoarte vechi - trebuie sa adaug tipul coloanei (dbDef) la toate custom columns deja definite (in serializare)

namespace App\Reports\Sources;

abstract class ColumnsManagerAbstract
{

	/**
	 * Associative array of all standard columns as defined in the current report source.
	 * @var array
	 */
	protected array $columnsCfg = [];

	/**
	 * Associative array of standard columns (the key is the column code).
	 * @var array
	 */
	protected array $stdColumns = [];

	/**
	 * Associative array of custom columns (the key is the column code).
	 * @var array
	 */
	protected array $customColumns = [];

	/**
	 * The report table primary key column.
	 * @var string
	 */
	protected string $primaryKeyColumn;

	/**
	 * The columns that are forced into the report.
	 * @var array
	 */
	protected array $forcedColumns;

	/**
	 * The columns dependencies list.
	 * @var array
	 */
	protected array $columnsDependencies;


	protected bool $dependenciesIncluded = false;

	/**
	 * Check that the columnsCfg are always defined.
	 */
	public function __construct()
	{
		if (!$this->columnsCfg) {
			throw new \Exception('This report source does not have the columnsCfg attribute defined: '.$this->getReportSource());
		}

		if (!$this->primaryKeyColumn) {
			throw new \Exception('This report source does not have the primary key attribute defined: '.$this->getReportSource());
		}

		// load columns langs
		\App\Loader_Lang::getInstance()->loadAppFile("_customLoad/reports/".lcfirst($this->getReportSource())."/columns.lang.php");

		// run plugin trigger - so we can extend/update the list of report columns for the current report source
		\Engine\Plugins\Manager::getInstance()->executeEvent("Reports-Sources-Columns-[{$this->getReportSource()}]", ['columnsManager' => $this]);

	}


	/**
	 * Returns the report source based on the current class namespace.
	 * @return string
	 */
	public function getReportSource()
	{
		$ns = getClassNamespace(get_called_class());
		return substr($ns, strrpos($ns, '\\')+1);
	}


	/**
	 * Adds a new standard column.
	 * @param string $columnCode
	 */
	public function addStdColumn(string $code, ?string $name=null, bool $hidden=false) :void
	{
		if (!key_exists($code, $this->columnsCfg)) {
			throw new \App\Reports\Sources\Exception("Invalid column code `{$code}`.");
		}

		if (key_exists($code, $this->stdColumns)) {
			throw new \App\Reports\Sources\Exception("Duplicated std column code `{$code}`.");
		}

		if ($name===null) {
			$_LANG = \App\Loader_Lang::getInstance()->getGlobal();
			$name = $_LANG['reports']['columnNames'][lcfirst($this->getReportSource())][$code];
		}

		// associates the main details for the standard columns but also adds the formula if set
		$columnDetails = [
			'type' => $this->columnsCfg[$code]['type'],
			'dbDef' => $this->columnsCfg[$code]['dbDef'],
			'name' => $name,
			'hidden' => $hidden,
		];
		if (key_exists('formula', $this->columnsCfg[$code])) {
			$columnDetails['formula'] = $this->columnsCfg[$code]['formula'];
		}

		$this->stdColumns[$code] = $columnDetails;

	}

	/**
	 * Returns the standard column config details.
	 * @param string $code
	 * @return array|NULL
	 */
	public function getStdColumnConfig(string $code) :?array
	{
		if (!key_exists($code, $this->columnsCfg)) {
			return null;
		}
		return $this->columnsCfg[$code];
	}

	public function getStdColumnDetails(string $code) :?array
	{
		if (key_exists($code, $this->stdColumns)) {
			return $this->stdColumns[$code];
		} else {
			return null;
		}
	}

	public function addCustomColumn_math(string $columnCode, string $columnName, string $columnType, string $calculation, ?bool $hidden=false) :void
	{
		// checks col - throws exc if issued found
		$this->checkCustomColumnCode($columnCode);

		// check dbDef - throws exception if invalid
		$dbDef = $this->getDbDefByCustomColumnType($columnType);

		// check math formula
		if ($this->getCalculationOperands($calculation)===false) {
			throw new \App\Reports\Sources\Exception("Incorrect math formula for column `{$columnCode}`.");
		}

		// add custom column
		$this->customColumns[$columnCode] = [
			'type' => $columnType,
			'dbDef' => $dbDef,
			'name' => $columnName,
			'hidden' => (bool)$hidden,
			'formula' => [
				'type'=>'math',
				'calculation' => $calculation,
			],
		];
	}


	public function addCustomColumn_entity(string $columnCode, string $columnName, string $columnType, string $entity, string $id, string $attribute, ?bool $hidden=false)
	{

		// checks col - throws exc if issued found
		$this->checkCustomColumnCode($columnCode);

		// check dbDef - throws exception if invalid
		$dbDef = $this->getDbDefByCustomColumnType($columnType);

		// check other details regarding entity formula
		$this->checkEntityFormula($entity, $id, $attribute);

		// add custom column
		$this->customColumns[$columnCode] = [
			'type' => $columnType,
			'dbDef' => $dbDef,
			'name' => $columnName,
			'hidden' => (bool)$hidden,
			'formula' => [
				'type' => 'entity',
				'entity' => $entity,
				'id' => $id,
				'attribute' => $attribute
			]
		];

	}

	/**
	 * Adds custom columns that can work by extracting a value from an associative array; mainly used
	 * for remarks or extraInfo fields.
	 * @param string $columnCode
	 * @param string $columnType
	 * @param string $sourceColumnCode
	 * @param string $key
	 */
	public function addCustomColumn_arrayValue(string $columnCode, string $columnName, string $columnType, string $sourceColumnCode, string $key, ?bool $hidden=false)
	{
		// checks col - throws exc if issued found
		$this->checkCustomColumnCode($columnCode);

		// check dbDef - throws exception if invalid
		$dbDef = $this->getDbDefByCustomColumnType($columnType);

		// checks source and key
		$this->checkArrayValueFormula($sourceColumnCode, $key);

		// add custom column
		$this->customColumns[$columnCode] = [
			'type' => $columnType,
			'dbDef' => $dbDef,
			'name' => $columnName,
			'hidden' => (bool)$hidden,
			'formula' => [
				'type' => 'arrayValue',
				'source' => $sourceColumnCode,
				'key' => $key
			]
		];
	}

	public function getCustomColumnDetails(string $code) :?array
	{
		if (key_exists($code, $this->customColumns)) {
			return $this->customColumns[$code];
		} else {
			return null;
		}
	}

	/**
	 * Check the custom column code.
	 * @param string $columnCode
	 */
	private function checkCustomColumnCode(string $columnCode) :void
	{
		// check column code
		if (!$columnCode) {
			throw new \App\Reports\Sources\Exception("Empty custom column code.");
		}
		if (!preg_match("/^[a-zA-Z]+[a-zA-Z0-9_]+$/", $columnCode)) {
			throw new \App\Reports\Sources\Exception("Invalid custom column code `{$columnCode}` (only letters, digits and underscore is allowed and must start with letter).");
		}
		// must not be set as the name of a std column
		if (key_exists($columnCode, $this->columnsCfg)) {
			throw new \App\Reports\Sources\Exception("Custom column cannot have the code `{$columnCode}` as this is a standard column.");
		}
		// check for duplicates
		if (key_exists($columnCode, $this->customColumns)) {
			throw new \App\Reports\Sources\Exception("Duplicated custom column code `{$columnCode}`.");
		}
	}

	/**
	 * Returns the DB definition based on the type of column.
	 * @param string $columnType
	 * @return string
	 */
	public function getDbDefByCustomColumnType(string $columnType) :string
	{

		switch ($columnType) {
			case 'boolean':
				return "TINYINT UNSIGNED NOT NULL DEFAULT 0";
			case 'int':
				return "BIGINT NOT NULL DEFAULT 0";
//			case 'float': // there are issues with this way mysql stores FLOATs so we replaced them with DECIMALs; try storing the value 17274.34 and you'll see that mysql stores 17274.3
//				return "DECIMAL(16,3) NOT NULL DEFAULT 0"; // TODO: this format is used only for math custom columns but it will be replaced once we refactor the reports so that the user can choose the type of column (FLOAT should and will not be available, PRICE is a better)
			case 'price':
				return "DECIMAL(16,3) NOT NULL DEFAULT 0";
			case 'varchar10':
				return "VARCHAR(10) NOT NULL DEFAULT ''";
			case 'varchar25':
				return "VARCHAR(25) NOT NULL DEFAULT ''";
			case 'varchar50':
				return "VARCHAR(50) NOT NULL DEFAULT ''";
			case 'varchar100':
				return "VARCHAR(100) NOT NULL DEFAULT ''";
			case 'varchar150':
				return "VARCHAR(150) NOT NULL DEFAULT ''";
			case 'varchar200':
				return "VARCHAR(200) NOT NULL DEFAULT ''";
			case 'varchar250':
				return "VARCHAR(250) NOT NULL DEFAULT ''";
			case 'varchar300':
				return "VARCHAR(300) NOT NULL DEFAULT ''";
			case 'varchar400':
				return "VARCHAR(400) NOT NULL DEFAULT ''";
			case 'varchar500':
				return "VARCHAR(500) NOT NULL DEFAULT ''";
			case 'text':
				return "TEXT NOT NULL";
			case 'date': case 'yearMonth':
				return "DATE NULL DEFAULT NULL";
			case 'year':
				return "YEAR NULL DEFAULT NULL";
			case 'datetime':
				return "DATETIME NULL DEFAULT NULL";
			case 'percent':
				return "DECIMAL(10,7) NOT NULL DEFAULT 0";
			default:
				throw new \App\Reports\Sources\Exception('Invalid custom column type - '.$columnType);
		}

	}


	/**
	 * Indicates if a certain column code is included in both standard and custom columns.
	 * This is used for validating report columns as well.
	 */
	public function columnCodeExists($columnCode) : bool
	{
		return key_exists($columnCode, $this->stdColumns) or key_exists($columnCode, $this->customColumns);
	}

	/**
	 * Indicates if the dependencies were already imported.
	 * @return bool
	 */
	public function dependenciesAreIncluded() :bool
	{
		return $this->dependenciesIncluded;
	}

	/**
	 * Adds all needed extra columns that are dependencies for selected columns.
	 */
	public function includeColumnsDependencies() :void
	{

		// check that the method is called only once
		if ($this->dependenciesIncluded) {
			throw new \Exception('You should not call `'.__METHOD__. '` twice.');
		}

		// STEP 1 - INCLUDE THE PRIMARY KEY AND THE FORCED COLUMNS ////////////////////////////////
		if (!key_exists($this->primaryKeyColumn, $this->stdColumns)) {
			$this->addStdColumn($this->primaryKeyColumn, null, true);
		}

		foreach ($this->forcedColumns as $colCode) {
			if (!key_exists($colCode, $this->stdColumns)) {
				$this->addStdColumn($colCode, null, true);
			}
		}

		// STEP 2 - INCLUDE THE CUSTOM COLUMNS DEPENDENCIES (or std cols with formulas) ///////////
		// forces both custom columns and standard columns with formulas (to be treated as custom columns)
		$allCols = array_merge($this->stdColumns, $this->customColumns);
		foreach ($allCols as $columnCode=>$columnDetails) {

			$columnFormula = $columnDetails['formula']??null;

			// skip cols with no formula (proabably standard simple cols)
			if (!$columnFormula) {
				continue;
			}

			switch ($columnFormula['type']) {

				case 'math':
					$mathOperands = $this->getCalculationOperands($columnFormula['calculation']);
					if ($mathOperands) {
						foreach ($mathOperands as $operand) {
							if (!$this->columnCodeExists($operand)) {
								$this->addStdColumn($operand, null, true);
							}
						}
					}
				break;

				case 'entity':
					if (!$this->columnCodeExists($columnFormula['id'])) {
						$this->addStdColumn($columnFormula['id'], null, true);
					}
				break;

				case 'arrayValue':
					if (!key_exists($columnFormula['source'], $this->stdColumns)) {
						$this->addStdColumn($columnFormula['source'], null, true);
					}
				break;
				default:
					throw new \Exception("Invalid type of formula `{$columnFormula['type']}` for custom column `{$columnCode}`.");
			}

		}

		// STEP 2 - INCLUDE DEPENDENCIES FROM COLUMN DEFINITION ///////////////////////////////////
		// we'll have to go in depth as the children of a column might have children on its own
		// loop through all selected columns to add dependencies and try again until no more dependencies found
		do {
			$foundDependency = false;
			// goes through each std column and checks its
			foreach ($this->stdColumns as $colCode=>$colDet) {
				if (key_exists($colCode, $this->columnsDependencies) and $this->columnsDependencies[$colCode]) {
					// add dependencies (that are not yet added)
					foreach ($this->columnsDependencies[$colCode] as $depColCode) {
						if (!key_exists($depColCode, $this->stdColumns)) {
							$foundDependency = true;
							$this->addStdColumn($depColCode, null, true);
						}
					}
				}
			}
		} while ($foundDependency);

		// mark that the dependencies were included
		$this->dependenciesIncluded = true;

	}


	/**
	 * Returns the operands of the math formula, or null if the formula is invalid.
	 * @param string  $formula
	 * @return array|NULL
	 */
	public function getCalculationOperands(string $formula) :array|false
	{

		// check if formula contains '%%'
		if (strpos($formula, '%%')!==false) {
			return false;
		}

		// list of all columun codes (standard and custom)
		$allColumnCodes = array_merge( array_keys($this->columnsCfg), array_keys($this->customColumns) );

		// replace numeric columns from formula with some random values
		$replacedFormula = $formula;
		$operandsList = [];
		foreach($allColumnCodes as $columnCode) {
			if (strpos($formula, '%'.$columnCode.'%')!==false) {
				$operandsList[] = $columnCode;
				$replacedFormula = str_replace('%'.$columnCode.'%', rand(100, 10000), $replacedFormula);
			}
		}

		// check that after the replacing of the operands, we get a correct arithmetic operation
		$cleanedReplacedFormula = preg_replace('/[^0-9\.\-\+\*\/\(\) ]/','', $replacedFormula);
		if ($cleanedReplacedFormula!=$replacedFormula) {
			return false;
		}

		// try to evaluate the formula
		try {
			eval($replacedFormula.';');
		} catch (\ParseError $e) {
			return false;
		}

		return $operandsList;

	}

	/**
	 * Chcks that the formula is correctly defined.
	 * @param string $entity
	 * @param string $id
	 * @param string $attribute
	 */
	public function checkEntityFormula(string $entity, string $id, string $attribute)
	{

		// check entity type
		$class = "\\App\\ReportsData\\EntityFields\\".$entity;
		if (!class_exists($class)) {
			throw new \App\Reports\Sources\CustomColumnFormulaException( sprintf('Invalid formula for entity custom column - invalid entity type; entity=`%s`, id=`%s`, attr=`%s`.', $entity, $id, $attribute) );
		}

		// check entity id - it must be either a std column (in the list of configured columns, that might be included due to dependencies) or a previously defined custom column
		if (!key_exists($id, $this->columnsCfg) and !$this->getCustomColumnDetails($id)) {
			throw new \App\Reports\Sources\CustomColumnFormulaException( sprintf('Invalid formula for entity custom column - invalid entity id column code; entity=`%s`, id=`%s`, attr=`%s`.', $entity, $id, $attribute) );
		}

		// main RD entity call - we initialize with 0 - just to skip a possible useless SQL in the DB
		$entObj = new $class(0);

		// get the attributes as array
		$attributes = explode('.', $attribute);

		// execute the attribute one by one
		$result = $entObj;
		foreach($attributes as $attr) {

			// check if the current object is still an Entity extension
			if ( !($result instanceof \App\ReportsData\EntityFields\GeneralAbstract) ) {
				throw new \App\Reports\Sources\CustomColumnFormulaException( sprintf('Invalid formula for entity custom column - invalid class type; entity=`%s`, id=`%s`, attr=`%s`.', $entity, $id, $attribute) );
			}
			try {
				$result = $result->getValue($attr);
			} catch (\Exception $e) {
				throw new \App\Reports\Sources\CustomColumnFormulaException( sprintf('Invalid formula for entity custom column - invalid attribute access; entity=`%s`, id=`%s`, attr=`%s`. Error: %s', $entity, $id, $attribute, $e->getMessage()) );
			}
		}
		// if, in the end, we still have an object, kill the report
		if (is_object($result)) {
			throw new \App\Reports\Sources\CustomColumnFormulaException( sprintf('Invalid formula for entity custom column - the result must be a scalar value; entity=`%s`, id=`%s`, attr=`%s`.', $entity, $id, $attribute) );
		}

	}

	/**
	 * Checks the array value formula - esp. source column.
	 * @param string $sourceColumnCode
	 * @param string $key
	 */
	public function checkArrayValueFormula(string $sourceColumnCode, string $key)
	{

		// no validations for key -  we just want to make sure it's a string

		// the source column must exist in this source
		if (!key_exists($sourceColumnCode, $this->columnsCfg)) {
			throw new \App\Reports\Sources\CustomColumnFormulaException("Custom column of type `arrayValue` cannot have the source `{$sourceColumnCode}` as this is not a standard column.");
		}
		if (!in_array($this->columnsCfg[$sourceColumnCode]['content'], ['remarks', 'assocArray'])) {
			throw new \App\Reports\Sources\CustomColumnFormulaException("Custom column of type `arrayValue` cannot have the source `{$sourceColumnCode}` as this is not an associative array content column.");
		}

	}

	/**
	 * Returns the all the details of the associated standard columns.
	 * @return array
	 */
	public function getStdColumns() : array
	{
		return $this->stdColumns;
	}

	/**
	 * Returns the all the details of the associated custom columns.
	 * @return array
	 */
	public function getCustomColumns() : array
	{
		return $this->customColumns;
	}

	/**
	 * Returns the PK column.
	 * @return string
	 */
	public function getTablePkColumn() :string
	{
		return $this->primaryKeyColumn;
	}

	public function defineStdColumn(string $code, string $type, string $dbDef, string $category, array $formula, ?string $content=null, bool $disabled = false, string|bool $subtotals = false)
	{

		// check if the column code is set
		if (!strlen($code)) {
			throw new \Exception('Unset column code in '.__METHOD__);
		}
		// synthetic validation that column is not already defined; we might be able to allow it but we do this to catch programmer error
		if (key_exists($code, $this->columnsCfg)) {
			throw new \Exception("Column `{$code}` is defined twice in ".get_called_class());
		}

		// populate the details of the current column - allows to overwrite the standard columns, maybe it's useful at some point
		$newColumn = [
			'type'		=> $type,
			'dbDef'		=> $dbDef,
			'cat'		=> $category,
		];
		// add extra info on the column
		if ($formula) {
			$newColumn['formula'] = $formula;
		}
		if ($content) {
			$newColumn['content'] = $content;
		}
		if ($disabled) {
			$newColumn['disabled'] = $disabled;
		}
		if ($subtotals) {
			$newColumn['subtotals'] = $subtotals;
		}

		$this->columnsCfg[$code] = $newColumn;
	}


	/**
	 * Imports standard columns from an assoc array of code=>name (name can be null and the translation is automatically loaded).
	 * @param array $stdColumns
	 */
	public function importStdColumnsFromArray(array $stdColumns) :void // TODO: [notImportant] we should implement export also - to be used when starting a new report
	{
		foreach ($stdColumns as $columnCode=>$columnName) {
			$this->addStdColumn($columnCode, $columnName);
		}
	}

	/**
	 * Imports custom columns from an assoc array of code=>details.
	 * @param array $customColumns
	 */
	public function importCustomColumnsFromArray(array $customColumns) :void // TODO: [notImportant] we should implement export also - to be used when starting a new report
	{

		foreach ($customColumns as $columnCode=>$columnDetais) {

			$columnFormula = $columnDetais['formula'];

			switch ($columnFormula['type']) {
				case 'math':
					$this->addCustomColumn_math($columnCode, $columnDetais['name'], $columnDetais['type'], $columnFormula['calculation'], $columnDetais['hidden']);
				break;
				case 'arrayValue':
					$this->addCustomColumn_arrayValue($columnCode, $columnDetais['name'], $columnDetais['type'], $columnFormula['source'], $columnFormula['key'], $columnDetais['hidden']);
				break;
				case 'entity':
					$this->addCustomColumn_entity($columnCode, $columnDetais['name'], $columnDetais['type'], $columnFormula['entity'], $columnFormula['id'], $columnFormula['attribute'], $columnDetais['hidden']);
				break;
				default:
					\Log::getInstance()->error('Invalid formula for custom column.', [
						'columnCode' => new \Engine\Logger\Context\StringRaw($columnCode),
						'columnDetails' => new \Engine\Logger\Context\ArrayAssoc($columnDetais),
					]);
					throw new \App\Reports\Sources\CustomColumnFormulaException("Invalid formula type `{$columnFormula['type']}` when importing custom column from array.");
			}


		}

	}

	/**
	 * Includes standard columns in the current object as dependencies based on custom filters.
	 * @param array $customFilters A linear list of columns from all custom filters definitions.
	 * @return void
	 */
	public function includeCustomFilterDependencies(array $columnsFromCustomFilters): void
	{
		foreach ($columnsFromCustomFilters as $columnCode) {
			if (!str_starts_with($columnCode, 'CUSTOM_COLUMN_')) {
				if (!key_exists($columnCode, $this->stdColumns)) {
					$this->addStdColumn($columnCode, null, true);
				}
			}
		}
	}

	/**
	 * Imports standard columns from a report template into the current configuration.
	 * For the hidden flag we rely on the "inc(included)" info.
	 * @param array $stdColumns An associative array of standard columns in the reportTemplate format.
	 * @return void
	 */
	public function importStdColumnsFromReportTemplate(array $stdColumns) :void
	{
		foreach ($stdColumns as $columnCode=>$columnDetails) {
			$hidden = !in_array($columnDetails['inc'], ['selected', 'forced']);
			$this->addStdColumn($columnCode, $columnDetails['name'], $hidden);
		}
	}

	/**
	 * Outputs the collected std columns to be imported in a report generation object.
	 * @return array
	 */
	public function exportStdColumnsToReportGeneration() :array
	{
		$return = [];
		foreach ($this->stdColumns as $columnCode=>$columnDetails) {
			if (!$columnDetails['hidden']) {
				$return[$columnCode] = $columnDetails['name'];
			}
		}
		return $return;
	}

	/**
	 * Imports custom columns from a report template into the current configuration.
	 * For the hidden flag we rely on the "inc(included)" info.
	 * @param array $customColumns An associative array of custom columns where each key is the column code,
	 *                              and the value contains details such as name, type, visibility, and formula configuration.
	 * @return void
	 * @throws \Exception If an invalid formula type is encountered for any custom column.
	 */
	public function importCustomColumnsFromReportTemplate(array $customColumns) :void // TODO: this is not used for now...
	{
		foreach ($customColumns as $columnCode=>$columnDetails) {
			$hidden = !in_array($columnDetails['inc'], ['selected', 'forced']);
			$formula = $columnDetails['formula'];

			if ($formula['type']=='math') { // math formula
				$this->addCustomColumn_math($columnCode, $columnDetails['name'], $columnDetails['type'], $formula['calculation'], $hidden);
			} elseif ($formula['type']=='entity') { // entity formula
				$this->addCustomColumn_entity($columnCode, $columnDetails['name'], $columnDetails['type'], $formula['entity'], $formula['id'], $formula['attribute'], $hidden);
			} elseif ($formula['type']=='arrayValue') { // array value
				$this->addCustomColumn_arrayValue($columnCode, $columnDetails['name'], $columnDetails['type'], $formula['source'], $formula['key'], $hidden);
			} else { // invalid formula
				throw new \Exception("Invalid formula type `{$formula['type']}` for custom column `{$columnCode}`.");
			}

		}
	}

	/**
	 * Sets the hidden flag for a specified column (both standard and custom).
	 * @param string $columnCode The unique identifier for the column.
	 * @param bool $hidden Indicates whether the column should be hidden (true) or visible (false).
	 * @return void
	 * @throws \App\Reports\Sources\Exception If the provided column code is invalid.
	 */
	public function setColumnHiddenFlag(string $columnCode, bool $hidden): void
	{
		if ($this->getStdColumnDetails($columnCode)) {
			$this->stdColumns[$columnCode]['hidden'] = $hidden;
		} elseif ($this->getCustomColumnDetails($columnCode)) {
			$this->customColumns[$columnCode]['hidden'] = $hidden;
		} else {
			throw new \App\Reports\Sources\Exception("Invalid column code `{$columnCode}` when trying to set hidden flag.");
		}
	}

	/**
	 * Retrieves all columns, ordered based on the provided list. Each column contains the full
	 * details stored internally.
	 *
	 * @param array $orderColumns A list of column codes defining the desired order. Missing columns are ignored.
	 * @return array An associative array of ordered columns. Columns specified in $orderColumns appear first, followed by any remaining columns.
	 */
	public function getAllOrderedColumns(array $orderColumns): array
	{
		$columnsList = [];

		$stdCols = $this->getStdColumns();
		$custCols = $this->getCustomColumns();

		// collect std and custom columns as explicitly ordered
		foreach($orderColumns as $columnCode) {
			if (key_exists($columnCode, $stdCols)) {
				$columnsList[$columnCode] = $stdCols[$columnCode];
				unset($stdCols[$columnCode]);
			} elseif (key_exists($columnCode, $custCols)) {
				$columnsList[$columnCode] = $custCols[$columnCode];
				unset($custCols[$columnCode]);
			} // there might be missing columns in the list - we ignore those
		}

		// collect remaining std and custom cols
		$columnsList += $stdCols;
		$columnsList += $custCols;

		return $columnsList;
	}

}
