<?php





namespace D1medrol\MysqlVector;
 
class VectorTable
{
    private string $name;
    private int $dimension;
    private string $engine;
    private array $centroidCache;
	private bool $normalizedMode = false;
    private \mysqli $mysqli;
    private int $quantizationSampleSize;
	private $filters = null;
	
    







    public function __construct(\mysqli $mysqli, string $name, int $dimension = 384, string $engine = 'InnoDB')
    {
        $this->mysqli = $mysqli;
        $this->name = $name;
        $this->dimension = $dimension;
        $this->engine = $engine;
        $this->centroidCache = [];
    }

    



    public function getVectorTableName(): string
    {
        return sprintf('%s_vectors', $this->name);
    }

    





    protected function getCreateStatements(bool $ifNotExists = true, bool $isMeta = false): array {
        $binaryCodeLengthInBytes = ceil($this->dimension / 8);

        $vectorsQuery =
         "CREATE TABLE %s %s (
            vector_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            vector MEDIUMBLOB,
            normalized_vector MEDIUMBLOB,
            magnitude DOUBLE,
            binary_code BINARY(%d),
            is_deleted TINYINT(1) NOT NULL DEFAULT 0,
            created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

            INDEX idx_is_deleted (is_deleted),
            INDEX idx_binary_code (binary_code(%s) )
        ) ENGINE=%s;";
        
		$vectorsQueries[] = sprintf($vectorsQuery, $ifNotExists ? 'IF NOT EXISTS' : '', $this->getVectorTableName(), $binaryCodeLengthInBytes, $binaryCodeLengthInBytes, $this->engine);
		
		if($isMeta){
			$metaQuery = 
			"CREATE TABLE %s `%s_meta` (
                `meta_id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                `vector_id` INT UNSIGNED,
                `item_id` INT UNSIGNED DEFAULT 0,
                `section_id` INT UNSIGNED DEFAULT NULL,
                `meta` JSON DEFAULT NULL,
                `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
				UNIQUE KEY `uniq_vector_id` (`vector_id`),
				KEY `idx_section_item` (`section_id`, `item_id`)
            ) ENGINE=%s;";
			
			$vectorQueries[] = $vectorsQueries[] = sprintf($metaQuery, $ifNotExists ? 'IF NOT EXISTS' : '', $this->getVectorTableName(), $this->engine);
		}
		
        return $vectorsQueries;
    }

    




    public function vectorToHex(array $vector): string {
        $binary = '';
        foreach($vector as $value) {
            $binary .= $value > 0 ? '1' : '0';
        }

        $padded = str_pad($binary, ceil(strlen($binary) / 8) * 8, '0', STR_PAD_LEFT);

        return $this->binaryToHexadecimal($padded);
    }

    




    private function binaryToHexadecimal(string $binaryString): string {
        $hex = '';
        foreach(str_split($binaryString, 4) as $char) {
            $hex .= strtoupper(dechex(bindec($char)));
        }
        $hex = str_pad($hex, ceil(strlen($hex) / 4) * 4, '0', STR_PAD_LEFT);
        return $hex;
    }
	
	



	public function setNormalizedMode($status = false){
		$this->normalizedMode = $status;
	}
	
	
    






    public function initialize(bool $ifNotExists = true, bool $isMeta = false): void
    {
        $this->mysqli->begin_transaction();
        foreach ($this->getCreateStatements($ifNotExists, $isMeta) as $statement) {
            $success = $this->mysqli->query($statement);
            if (!$success) {
                $e = new \Exception($this->mysqli->error);
                $this->mysqli->rollback();
                throw $e;
            }
        }
        $this->mysqli->commit();

    }

    






    public function cosim(array &$v1, array &$v2): float
    {
        $length = count($v1);
		if($length != count($v2))
			return 0.0;
		
		$sum = 0.0;
		for($i=0;$i < $length; $i++){
			$sum += $v1[$i] * $v2[$i];
		}
		
		 
		if(!$this->normalizedMode){
			$sum = $sum / ( $this->getMagnitude($v1) * $this->getMagnitude($v2) );
		}
		
		return $sum;
		
    }
	
	






	public function upsertMeta(array $meta, int $vector_id): int
	{
		$tableName = $this->getVectorTableName() . '_meta';

		$jsonMeta = json_encode($meta, JSON_UNESCAPED_UNICODE);
		
		if (!$vector_id) {
			throw new \Exception('Vector_id is empty');
		}
		
		if ($jsonMeta === false) {
			throw new \Exception('Failed to encode meta as JSON');
		}

		$query = "
			INSERT INTO $tableName (
				vector_id,
				item_id,
				section_id,
				meta
			) VALUES (?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
				meta_id = LAST_INSERT_ID(meta_id),
				item_id = VALUES(item_id),
				section_id = VALUES(section_id),
				meta = VALUES(meta)
		";

		$statement = $this->mysqli->prepare($query);

		if (!$statement) {
			throw new \Exception($this->mysqli->error);
		}

		$item_id = (int)($meta['item_id'] ?? 0);

		$section_id = array_key_exists('section_id', $meta)
			? (int)$meta['section_id']
			: null;

		$statement->bind_param(
			'iiis',
			$vector_id,
			$item_id,
			$section_id,
			$jsonMeta
		);

		$success = $statement->execute();

		if (!$success) {
			$error = $statement->error;
			$statement->close();

			throw new \Exception($error);
		}

		$meta_id = (int)$statement->insert_id;

		$statement->close();

		return $meta_id;
	}
	
    






    public function upsert(array $vector, ?int $id = null): int
    {
        $magnitude = $this->getMagnitude($vector);
        $normalizedVector = $this->normalize($vector, $magnitude);
        $binaryCode = $this->vectorToHex($normalizedVector);
        $tableName = $this->getVectorTableName();

        $insertQuery = empty($id) ?
            "INSERT INTO $tableName (vector, normalized_vector, magnitude, binary_code) VALUES (?, ?, ?, UNHEX(?))" :
            "UPDATE $tableName SET vector = ?, normalized_vector = ?, magnitude = ?, binary_code = UNHEX(?) WHERE id = $id";

        $statement = $this->mysqli->prepare($insertQuery);
        if(!$statement) {
            $e = new \Exception($this->mysqli->error);
            $this->mysqli->rollback();
            throw $e;
        }

        $vectorBlob = pack('f*', ...$vector);
        $normalizedVectorBlob = pack('f*', ...$normalizedVector);

		$statement->bind_param('ssds', $vectorBlob, $normalizedVectorBlob, $magnitude, $binaryCode);
		
        $success = $statement->execute();
        if(!$success) {
            throw new \Exception($statement->error);
        }

        $vector_id = $statement->insert_id;
        $statement->close();

        return $vector_id;
    }

    





    public function batchInsert(array $vectorArray): array {
        $tableName = $this->getVectorTableName();

        $statement = $this->getConnection()->prepare("INSERT INTO $tableName (vector, normalized_vector, magnitude, binary_code) VALUES (?, ?, ?, UNHEX(?))");
        if(!$statement) {
            throw new \Exception("Prepare failed: " . $this->getConnection()->error);
        }

        $ids = [];
        $this->getConnection()->begin_transaction();
        try {
            foreach ($vectorArray as $vector) {
                $magnitude = $this->getMagnitude($vector);
                $normalizedVector = $this->normalize($vector, $magnitude);
                $binaryCode = $this->vectorToHex($normalizedVector);
				$vectorBlob = pack('f*', ...$vector);
				$normalizedVectorBlob = pack('f*', ...$normalizedVector);

                $statement->bind_param('ssds', $vectorBlob, $normalizedVectorBlob, $magnitude, $binaryCode);

                if (!$statement->execute()) {
                    throw new \Exception("Execute failed: " . $statement->error);
                }

                $ids[] = $statement->insert_id;
            }

            $this->getConnection()->commit();
        } catch (\Exception $e) {
            $this->getConnection()->rollback();
            throw $e;
        } finally {
            $statement->close();
        }

        return $ids;
    }

    





    public function select(): \Generator {
        $tableName = $this->getVectorTableName();
		
		$sql = "SELECT * FROM $tableName WHERE 1";
		
		 
		if($this->filters && is_string($this->filters)){
			$sql .= " AND vector_id IN (?)";
		}
		
        $statement = $this->mysqli->prepare($sql);
		
		if($this->filters)
			$statement->bind_param('s', trim($this->filters) );
		
        if (!$statement) {
            $e = new \Exception($this->mysqli->error);
            $this->mysqli->rollback();
            throw $e;
        }

        $statement->execute();
        $result = $statement->get_result();
		
        while ($row = $result->fetch_assoc()) {
            yield $row;
        }

    }

    



    public function count(): int {
        $tableName = $this->getVectorTableName();
        $statement = $this->mysqli->prepare("SELECT COUNT(id) FROM $tableName");
        $statement->execute();
        $statement->bind_result($count);
        $statement->fetch();
        $statement->close();
        return $count;
    }

    




    private function getMagnitude(array $vector): float
    {
        $sum = 0;
        foreach ($vector as $value) {
            $sum += $value * $value;
        }

        return sqrt($sum);
    }

    






    public function searchHamming(array &$vector, int $n = 10): array {
        
		$tableName = $this->getVectorTableName();
         
        $binaryCode = $this->vectorToHex($vector);

         
        $statement = $this->mysqli->prepare("SELECT vector_id, BIT_COUNT(binary_code ^ UNHEX(?)) AS hamming_distance FROM $tableName WHERE %s ORDER BY hamming_distance LIMIT $n");
        $statement->bind_param('ss', $binaryCode, $this->filters && is_string($this->filters) ? " `vector_id` IN ({$this->filters}) " : ' 1 ');

        if(!$statement) {
            $e = new \Exception($this->mysqli->error);
            $this->mysqli->rollback();
            throw $e;
        }

        $statement->execute();
        $statement->bind_result($vectorId, $hd);

        $candidates = [];
        while ($statement->fetch()) {
            $candidates[] = ['vector_id' => $vectorId, 'score' => $hd];
        }
        $statement->close();

        return $candidates;
    }
	
	





	public function applyFilters($filters = ''){
		$this->filters = $filters;
	}
	
	



	public function clearFilters(){
		$this->applyFilters(null);
	}
	
	





	public function searchCosim(array &$vector, int $k = 5): array {
		
		$heap = new \SplMinHeap();
		
		if(!$this->normalizedMode)
			$normalizedVector = $this->normalize($vector);
		else
			$normalizedVector = $vector;
		
		foreach ($this->select() as $row) {
			$vector2 = array_values(unpack('f*', $row['normalized_vector']));
			$score = $this->cosim($normalizedVector, $vector2);

			$heap->insert(['score' => $score, 'vector_id' => $row['vector_id']]);
			if ($heap->count() > $k)
				$heap->extract();
		}

		$results = [];
		foreach ($heap as $item) 
			$results[] = $item;
			
		return array_reverse($results);
		
	}
	
    






    private function normalize(array $vector, ?float $magnitude = null, float $epsilon = 1e-10): array {
        $magnitude = !empty($magnitude) ? $magnitude : $this->getMagnitude($vector);
        if ($magnitude == 0) {
            $magnitude = $epsilon;
        }
        foreach ($vector as $key => $value) {
            $vector[$key] = $value / $magnitude;
        }
        return $vector;
    }

    





	public function delete(int $vector_id): void
	{
		$tableName = $this->getVectorTableName();
		$metaTableName = $tableName . '_meta';

		$this->mysqli->begin_transaction();

		try {

			 
			$statement = $this->mysqli->prepare(
				"DELETE FROM $tableName WHERE vector_id = ?"
			);

			if (!$statement) {
				throw new \Exception($this->mysqli->error);
			}

			$statement->bind_param('i', $vector_id);

			if (!$statement->execute()) {
				throw new \Exception($statement->error);
			}

			$statement->close();

			 
			$checkStatement = $this->mysqli->prepare("
				SELECT COUNT(*)
				FROM information_schema.tables
				WHERE table_schema = DATABASE()
				AND table_name = ?
			");

			if (!$checkStatement) {
				throw new \Exception($this->mysqli->error);
			}

			$checkStatement->bind_param('s', $metaTableName);

			if (!$checkStatement->execute()) {
				throw new \Exception($checkStatement->error);
			}

			$checkStatement->bind_result($metaTableExists);
			$checkStatement->fetch();
			$checkStatement->close();

			 
			if ($metaTableExists > 0) {

				$metaStatement = $this->mysqli->prepare(
					"DELETE FROM $metaTableName WHERE vector_id = ?"
				);

				if (!$metaStatement) {
					throw new \Exception($this->mysqli->error);
				}

				$metaStatement->bind_param('i', $vector_id);

				if (!$metaStatement->execute()) {
					throw new \Exception($metaStatement->error);
				}

				$metaStatement->close();
			}

			$this->mysqli->commit();

		} catch (\Exception $e) {

			$this->mysqli->rollback();

			throw $e;
		}
	}

    



    public function getConnection(): \mysqli {
        return $this->mysqli;
    }
}
