<?php

namespace apexl\Io\services;

use apexl\ClassEngine\EngineSingleton;
use apexl\Vault\Vault;

class Database extends EngineSingleton
{
    protected static $batchSize = 100; // Number of updates per query
    protected static $tableDescriptions = [];
    protected static $modules = [];

    public static function reset()
    {
        parent::reset();
        self::$batchSize = 100;
        self::$tableDescriptions = [];
        self::$modules = [];
    }

    /**
     * Describe a table - doing it here allows caching of the results to avoid far too many SQL Describe calls
     * @param $tableName
     * @param $forceRefresh
     * @return mixed
     */
    public static function describe($tableName, $forceRefresh = false)
    {
        if (!isset(self::$tableDescriptions[$tableName]) || $forceRefresh) {
            $vault = Vault::getInstance();
            self::$tableDescriptions[$tableName] = $vault->newQuery()->describe($tableName)->fetchAll();
        }

        return self::$tableDescriptions[$tableName] ?? null;
    }

    /**
     * Get modules - again doing it this way allows caching
     * @param $tableName
     * @param $forceRefresh
     * @return mixed
     */
    public static function getModule($namespace)
    {
        if (empty(self::$modules)) {
            $vault = Vault::getInstance();
            $rows = $vault->select('modules')->fields()->execute()->fetchAll();
            foreach ($rows as $row) {
                self::$modules[$row->namespace] = (array) $row;
            }
        }

        return self::$modules[$namespace] ?? null;
    }

    /**
     * Persist an array of entities - can contain a mixture of inserts and updates
     * @throws \Exception
     */
    public static function persistEntities(array $entities): int
    {
        self::validateData($entities);
        $chunks = array_chunk($entities, self::$batchSize);
        foreach ($chunks as $chunk) {
            self::persistBatch($chunk); // carry out insert
        }

        return count($entities);
    }

    /**
     * Check that only one entity type is provided
     * @param $entities
     * @throws \Exception
     */
    private static function validateData($entities)
    {
        $entityClasses = [];
        foreach ($entities as $entity) {
            $entityClasses[$entity::class] = 1;
        }

        $numClasses = count($entityClasses);
        if ($numClasses !== 1) { // make sure only one type of entity, else throw an error
            throw new \Exception(
                "Data provided to updateEntities must be of one and only one entity-type; $numClasses given: (" . implode(
                    "; ",
                    array_flip($entityClasses)
                ) . ").", 500
            );
        }
    }

    /**
     * Persist a batch of entities to the database - can contain a mixture of inserts and updates
     * (Vault uses INSERT ON DUPLICATE KEY UPDATE syntax)
     */
    public static function persistBatch(array $entities)
    {
        if ($entities !== []) {
            $firstEntity = $entities[0];
            $vault = Vault::getInstance();
            $data = self::getDataFromEntities($entities);
            $vault->insert($firstEntity->getTableName())->fields($data)->execute();
        }

        return;
    }

    /**
     * Get data from the entity and put it in the right format for Vault
     * @param $entities
     * @return array
     */
    private static function getDataFromEntities($entities)
    {
        $batchData = [];
        foreach ($entities as $entity) {
            $entity->getEntityDataFields();
            $entity->setStandardFields();
            $columns = $entity->getTableColumns();

            $data = $entity->getData(false);
            //store and strip metadata if it's available. Allows entities to contain processing and maintain easy store methods.
            if (isset($data['_metadata'])) {
                unset($data['_metadata']);
            }

            //check for any extra fields that we don't have in the schema
            foreach ($data as $field => $value) {
                if (!isset($columns[$field])) {
                    unset($data[$field]);
                }
            }
            $batchData[] = $data;
        }

        return $batchData;
    }

    /**
     * Persist Raw data, can contain a mixture of inserts and updates
     * @param $data
     * @param $tableName
     */
    public static function persistRawBatch($data, $tableName): int
    {
        $chunks = array_chunk($data, self::$batchSize);
        $vault = Vault::getInstance();
        foreach ($chunks as $chunk) {
            $vault->insert($tableName)->fields($chunk)->execute();
        }

        return count($chunks);
    }
}
