<?php

namespace engine;

use ReflectionClass;
use ReflectionProperty;

abstract class Model
{
    const DB_TABLE = null;
    const DB_KEY = 'id';

    /** @var $SpecCache ModelSpec[] */
    protected static array $SpecCache = [];

    public static function create_instance(...$args) {
        $cl = get_called_class();
        return new $cl(...$args);
    }

    public function __construct(array $raw) {
        if (!isset(self::$SpecCache[static::class]))
            self::$SpecCache[static::class] = static::get_spec();

        foreach (self::$SpecCache[static::class]->getProperties() as $prop)
            $this->{$prop->getModelName()} = $prop->fromRawValue($raw[$prop->getDbName()]);

        if (is_null(static::DB_TABLE))
            trigger_error('class '.get_class($this).' doesn\'t have DB_TABLE defined');
    }

    /**
     * TODO: support adding or subtracting (SET value=value+1)
     */
    public function edit(array $fields) {
        $db = getDB();

        $model_upd = [];
        $db_upd = [];

        $spec_db_name_map = self::$SpecCache[static::class]->getDbNameMap();
        $spec_props = self::$SpecCache[static::class]->getProperties();

        foreach ($fields as $name => $value) {
            $index = $spec_db_name_map[$name] ?? null;
            if (is_null($index)) {
                logError(__METHOD__.': field `'.$name.'` not found in '.static::class);
                continue;
            }

            $field = $spec_props[$index];
            if ($field->isNullable() && is_null($value)) {
                $model_upd[$field->getModelName()] = $value;
                $db_upd[$name] = $value;
                continue;
            }

            switch ($field->getType()) {
                case ModelFieldType::ARRAY:
                    if (is_array($value)) {
                        $db_upd[$name] = implode(',', $value);
                        $model_upd[$field->getModelName()] = $value;
                    } else {
                        logError(__METHOD__.': field `'.$name.'` is expected to be array. skipping.');
                    }
                    break;

                case ModelFieldType::INTEGER:
                    $value = (int)$value;
                    $db_upd[$name] = $value;
                    $model_upd[$field->getModelName()] = $value;
                    break;

                case ModelFieldType::FLOAT:
                    $value = (float)$value;
                    $db_upd[$name] = $value;
                    $model_upd[$field->getModelName()] = $value;
                    break;

                case ModelFieldType::BOOLEAN:
                    $db_upd[$name] = $value ? 1 : 0;
                    $model_upd[$field->getModelName()] = $value;
                    break;

                case ModelFieldType::JSON:
                    $db_upd[$name] = jsonEncode($value);
                    $model_upd[$field->getModelName()] = $value;
                    break;

                case ModelFieldType::SERIALIZED:
                    $db_upd[$name] = serialize($value);
                    $model_upd[$field->getModelName()] = $value;
                    break;

                default:
                    $value = (string)$value;
                    $db_upd[$name] = $value;
                    $model_upd[$field->getModelName()] = $value;
                    break;
            }
        }

        if (!empty($db_upd) && !$db->update(static::DB_TABLE, $db_upd, static::DB_KEY."=?", $this->get_id())) {
            logError(__METHOD__.': failed to update database');
            return;
        }

        if (!empty($model_upd)) {
            foreach ($model_upd as $name => $value)
                $this->{$name} = $value;
        }
    }

    public function get_id() {
        return $this->{toCamelCase(static::DB_KEY)};
    }

    public function as_array(array $properties = [], array $custom_getters = []): array {
        if (empty($properties))
            $properties = static::$SpecCache[static::class]->getPropNames();

        $array = [];
        foreach ($properties as $field) {
            if (isset($custom_getters[$field]) && is_callable($custom_getters[$field])) {
                $array[$field] = $custom_getters[$field]();
            } else {
                $array[$field] = $this->{toCamelCase($field)};
            }
        }

        return $array;
    }

    protected static function get_spec(): ModelSpec {
        $rc = new ReflectionClass(static::class);
        $props = $rc->getProperties(ReflectionProperty::IS_PUBLIC);

        $list = [];
        $index = 0;

        $db_name_map = [];

        foreach ($props as $prop) {
            if ($prop->isStatic())
                continue;

            $name = $prop->getName();
            if (str_starts_with($name, '_'))
                continue;

            $real_type = null;
            $type = $prop->getType();
            $phpdoc = $prop->getDocComment();

            /** @var ?ModelFieldType $mytype */
            $mytype = null;
            if (!$prop->hasType() && !$phpdoc)
                $mytype = ModelFieldType::STRING;
            else {
                $typename = $type->getName();
                switch ($typename) {
                    case 'string':
                        $mytype = ModelFieldType::STRING;
                        break;
                    case 'int':
                        $mytype = ModelFieldType::INTEGER;
                        break;
                    case 'float':
                        $mytype = ModelFieldType::FLOAT;
                        break;
                    case 'array':
                        $mytype = ModelFieldType::ARRAY;
                        break;
                    case 'bool':
                        $mytype = ModelFieldType::BOOLEAN;
                        break;
                    default:
                        if (enum_exists($typename)) {
                            $mytype = ModelFieldType::BACKED_ENUM;
                            $real_type = $typename;
                        }
                        break;
                }

                if ($phpdoc != '') {
                    $pos = strpos($phpdoc, '@');
                    if ($pos === false)
                        continue;

                    if (substr($phpdoc, $pos + 1, 4) == 'json')
                        $mytype = ModelFieldType::JSON;
                    else if (substr($phpdoc, $pos + 1, 5) == 'array')
                        $mytype = ModelFieldType::ARRAY;
                    else if (substr($phpdoc, $pos + 1, 10) == 'serialized')
                        $mytype = ModelFieldType::SERIALIZED;
                }
            }

            if (is_null($mytype))
                logError(__METHOD__.": ".$name." is still null in ".static::class);

            // $dbname = from_camel_case($name);
            $model_descr = new ModelProperty(
                type: $mytype,
                realType: $real_type,
                nullable: $type->allowsNull(),
                modelName: $name,
                dbName: fromCamelCase($name)
            );
            $list[] = $model_descr;
            $db_name_map[$model_descr->getDbName()] = $index++;
        }

        return new ModelSpec($list, $db_name_map);
    }
}
