<?php

namespace Snaju\Inception\ORM;

use DateTime;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\DataModelRestObject;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\Mapping\MappingException;
use Laminas\Code\Reflection\ClassReflection;
use Laminas\Code\Reflection\MethodReflection;
use Snaju\Inception\Exception\Exception;
use Snaju\Inception\Exception\InvalidArgumentException;
use Snaju\Inception\Route\Loaders\Twig\TemplateManager;
use Snaju\Inception\Util\ArrayUtil;
use Snaju\Inception\Util\Loaders\Stack;
use Snaju\Inception\Util\Util;

abstract class DataModel implements \ArrayAccess
{

    protected $_d;

    protected $meta = [];

    protected $__hydrated = false;

    public static function get($id)
    {
        return ORM::$EntityManager->find(self::getClassName(), $id);
    }

    public function getVal($paramName)
    {
        if (isset($this->{$paramName})) {
            return $this->{$paramName};
        }

        return null;
    }

    public function setVal($paramName, $val)
    {
        if (property_exists($this, $paramName)) {
            $this->{$paramName} = $val;

            return $this->{$paramName};
        }

        return null;
    }

    private static function multiId(): bool
    {
        $meta = ORM::$EntityManager->getClassMetadata(self::getClassName());

        if ($meta instanceof ClassMetadata) {
            try {

                $ids = $meta->getIdentifierColumnNames();

                if (count($ids) > 1) {
                    return true;
                }
            } catch (Exception $e) {
                // Do Nothing.
            }
        }

        return false;
    }

    public function setIfNotImmutable(string $property, $value)
    {
        if (property_exists($this, $property)) {
            if (!in_array($property, $this->immutable)) {
                // If the expected value is a date/generate date and set it
                if (in_array($property, $this->dates)) {
                    $value = $this->getDateTimeFromISOString($value);
                }

                // If expected value is, encode as a json string
                if (in_array($property, $this->json)) {
                    $value = json_encode($value);
                }

                $this->{$property} = $value;

                if (property_exists($this, 'updated_at')) {
                    $this->updated_at = new DateTime();
                }
            }
        }
    }

    public function getVars()
    {
        return get_object_vars($this);
    }

    public function getUUIDString()
    {

        $meta = $this->getEntityMeta();

        if ($meta instanceof ClassMetadata) {

            $objName = $meta->name;

            $idFinal = null;
            $id = $this->getIdField();
            if (is_array($id)) {
                foreach ($id as $ki => $vi) {
                    if ($vi instanceof DataModel) {
                        $vi = $vi->getIdField();
                    }

                    $idFinal .= "{$ki}:{$vi}|";
                }
            } else {
                $idFinal = $id;
            }

            return "Inception." . $objName . "." . $idFinal;
        }

        return "";
    }

    public function getIdField()
    {
        $meta = ORM::$EntityManager->getClassMetadata(self::getClassName());

        if ($meta instanceof ClassMetadata) {
            try {

                $ids = $meta->getIdentifierValues($this);

                if (count($ids) == 1) {
                    return $this->{$meta->getSingleIdentifierFieldName()};
                } else {
                    return $ids;
                }
            } catch (MappingException $e) {
            }
        }

        return null;
    }

    public function getEntityMeta(): ?ClassMetadata
    {
        $meta = ORM::$EntityManager->getClassMetadata(self::getClassName());

        if ($meta instanceof ClassMetadata) {

            return $meta;
        }

        return null;
    }

    public function getUuid()
    {
        $meta = ORM::$EntityManager->getClassMetadata(self::getClassName());

        if ($meta instanceof ClassMetadata) {
            try {
                return $meta->getIdentifier();
            } catch (MappingException $e) {
            }
        }

        return null;
    }

    public
    static function getTop(Criteria $criteria)
    {
        $objs = self::find($criteria);
        if (count($objs) > 0) {
            return $objs[0];
        }

        return null;
    }

    public
    static function find(Criteria $criteria)
    {
        return ORM::$EntityManager->getRepository(self::getClassName())->matching($criteria)->toArray();
    }

    public
    static function findAndDelete(Criteria $criteria)
    {
        $elms = self::find($criteria);
        foreach ($elms as $elm) {
            ORM::$EntityManager->remove($elm);
        }
        ORM::$EntityManager->flush();
    }

    public
    static function countQuery(Criteria $criteria)
    {
        return ORM::$EntityManager->getRepository(self::getClassName())->matching($criteria)->count();
    }

    public
    static function totalCount()
    {
        return ORM::$EntityManager->getRepository(self::getClassName())->matching(Criteria::create())->count();
    }

    public
    static function getRepository()
    {
        return ORM::$EntityManager->getRepository(self::getClassName());
    }

    public
    static function getAll()
    {
        if (ORM::$EntityManager->getRepository(self::getClassName()) != null) {
            return ORM::$EntityManager->getRepository(self::getClassName())->findAll();
        }

        return [];
    }

    public
    static function query($sqlQuery)
    {
        return ORM::$EntityManager->createQuery("select * from " . self::getClassName() . $sqlQuery);
    }

    public
    static function getClassName()
    {
        return get_called_class();
    }

    public static function getObjName()
    {
        $meta = ORM::$EntityManager->getClassMetadata(self::getClassName());
        if ($meta instanceof ClassMetadata) {
            return $meta->getName();
        }
        return null;
    }

    public
    static function getClassNameFromTableName(string $tableName): ?string
    {
        // Loop through all classes
        $em = ORM::getEntityManager();
        $classNames = $em->getConfiguration()->getMetadataDriverImpl()->getAllClassNames();

        foreach ($classNames as $className) {
            $classMetaData = $em->getClassMetadata($className);
            if ($tableName === $classMetaData->getTableName()) {
                return $classMetaData->getName();
            }
        }

        return null;
    }

    public
    static function getResourceId(string $separator = ' ', bool $lowercase = true): string
    {
        $class = self::getClassName();
        $id = isset($class::$resourceId) ? $class::$resourceId : '';

        // Fallback to class name if no resource id is set in the model
        if (empty($id)) {
            $parts = explode('\\', self::getClassName());
            $words = preg_split('/(?=[A-Z])/', lcfirst($parts[count($parts) - 1]));

            if (count($words) > 1) {
                $id = trim(implode($separator, $words));
            } else {
                $id = $words[0];
            }
        }

        // Apply lowercase if true
        if ($lowercase) {
            $id = strtolower($id);
        }

        return $id;
    }

    public
    static function currentTimeStamp()
    {
        return date("Y-m-d H:i:s", time());
    }

    public
    static function currentDateTime()
    {
        $dt = new \DateTime('now');
        return $dt;
    }

    public
    static function sum($field, Criteria $criteria = null)
    {
        $q = ORM::$EntityManager->createQueryBuilder();
        $q->from(self::getClassName(), "sq");
        $q->select("SUM(sq.$field) as sum");
        if ($criteria != null) {
            $q->addCriteria($criteria);
        }
        $r = $q->getQuery()->getOneOrNullResult();
        if (is_array($r)) {
            if ($r['sum'] == null) {
                return 0;
            } else {
                return $r['sum'];
            }
        }

        return 0.0;
    }

    public static function getQueryBuilder()
    {
        $q = ORM::$EntityManager->createQueryBuilder();
        $q->from(self::getClassName(), "qb");
        return $q;
    }

    private
    function objVars()
    {
        return array_merge(get_object_vars($this), $this->_d);
    }

    public
    function process($data = [])
    {
        $this->_d = $data;
    }

    public function persist()
    {
        ORM::$EntityManager->persist($this);
        return $this;
    }

    public function delete()
    {
        ORM::$EntityManager->remove($this);
        ORM::$EntityManager->flush($this);
        return $this;
    }

    public function flush()
    {
        ORM::$EntityManager->flush($this);
        return $this;
    }

    /**
     * Convenience method for calling persist() and flush().
     */
    public function save()
    {
        $this->persist();
        $this->flush();
        return $this;
    }

    public function toArray(Stack $stack = null, int $level = 0): DataModelArray
    {
        $this->toTemplateObject();
        return new DataModelArray($this, $stack, $level);
    }

    public function toTemplateObject(Stack $stack = null, bool $checkChildren = true): DataModel
    {

        $this->_hydrate();

//        if ($stack == null) {
//            $stack = new Stack();
//        }
//
//        $uuid = $this->getUUIDString();
//
//        if (!$stack->contains($uuid)) {
//
//            $stack->push($uuid);
//
//            if ($this instanceof DataModelProcessor) {
//                $this->preOnArray();
//                $arr = [];
//                $this->onToArray($arr);
//
//                $this->loadIntoObj($arr);
//
////                foreach (ArrayUtil::getDotArray($arr) as $k => $v) {
////                    TemplateManager::addData($k, $v);
////                }
//            }
//
//            if ($checkChildren) {
//                foreach ($this->getVars() as $var) {
//                    if ($var instanceof DataModel) {
//                        $var->toTemplateObject($stack, true);
//                    } else if ($var instanceof Collection) {
//                        foreach ($var as $var2) {
//                            if ($var2 instanceof DataModel) {
//                                $var2->toTemplateObject($stack, true);
//                            }
//                        }
//                    }
//                }
//            }
//        }

        return $this;
    }

    private function loadIntoObj($arr)
    {
        foreach ($arr as $k => $v) {
            if (isset($this->getVars()[$k])) {
                // Is a var.
                if ($this->{$k} instanceof DataModel) {
                    $this->{$k}->loadIntoObj($v);
                }
            } else {
                $this->{$k} = $v;
            }
        }
    }

    public function &getMetaArray(): array
    {
        return $this->meta;
    }

    public function mergeMetaArray(array $part)
    {
        $this->meta = array_merge($part, $this->meta);
    }

    public function setMetaArray(array &$arr)
    {
        $this->meta = $arr;
    }

    public
    function isEqualTo(DataModel $dataModel)
    {
        foreach ($this->getVars() as $k => $v) {
            if ($dataModel->getVal($k) == null) {
                return false;
            } else if ($dataModel->getVal($k) != $v) {
                return false;
            }
        }

        return true;
    }

    public
    function getDateTimeFromISOString(string $dateString): DateTime
    {
        $format = DateTime::ISO8601;
        return DateTime::createFromFormat($format, $dateString);
    }

    public
    function setDateProperty(string $property, string $value)
    {
        $format = DateTime::ISO8601;
        $dt = DateTime::createFromFormat($format, $value);
        $this->setIfNotImmutable($property, $dt);
    }

    public
    function setJsonProperty(string $property, string $value)
    {
        if (!Util::isJson($value)) {
            throw new InvalidArgumentException('Value passed to setJsonProperty must be a json string.', 400);
        }

        $json = json_encode($value);
        $this->setIfNotImmutable($property, $json);
    }

    private function callMagicFunction(string $name)
    {
        $c = new \ReflectionClass(self::getClassName());
        if ($c instanceof \ReflectionClass) {
            if ($c->hasMethod($name)) {
                $m = $c->getMethod($name);
                if ($m instanceof \ReflectionMethod) {
                    if ($m->isInternal() && $m->getNumberOfRequiredParameters() == 0) {
                        return $m->invoke($this);
                    }
                }
            }
        }
        return null;
    }

    private function magicMethodExist(string $name)
    {
        $c = new \ReflectionClass(self::getClassName());
        if ($c instanceof \ReflectionClass) {
            if ($c->hasMethod($name)) {
                $m = $c->getMethod($name);
                if ($m->isInternal() && $m instanceof \ReflectionMethod) {
                    if ($m->getNumberOfRequiredParameters() == 0) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public function __call(string $name, array $arguments)
    {
        $this->_hydrate();
        return $this->{$name};
    }

    public function __get($name)
    {
        $this->_hydrate();

        if (isset($this->meta[$name])) {
            if (is_resource($this->meta[$name])) {
                return stream_get_contents($this->meta[$name]);
            }

            return $this->meta[$name];
        }

        if ($this->magicMethodExist($name)) {
            return $this->callMagicFunction($name);
        }

        return null;
    }

    public function __set($name, $value)
    {
        $this->meta[$name] = $value;
    }

    public function __isset($name)
    {
        if (array_key_exists($name, $this->getVars())) {
            return true;
        }

        if (isset($this->meta[$name])) {
            return true;
        }

        if ($this->magicMethodExist($name)) {
            return true;
        }

        return false;
    }

    public function __toString()
    {
        return "InceptionDataModel." . self::getUUIDString();
    }

    public function _hydrate()
    {
        if ($this instanceof DataModelProcessor && !$this->__hydrated) {
            $this->preOnArray();
            $this->onToArray($this->meta);

            /*
             * Sift down items that are in children
             * */
            foreach ($this->getVars() as $var => $val) {
                if (isset($this->meta[$var]) && $val instanceof DataModel) {
                    $val->mergeMetaArray($this->meta[$var]);
                    unset($this->meta[$var]);
                }
            }

            $this->__hydrated = true;
        }
    }

    public function offsetExists($offset): bool
    {
        return (
            isset($this->getVars()[$offset]) ||
            isset($this->meta[$offset]) || $this->magicMethodExist($offset)
        );
    }

    public function offsetGet($offset)
    {
        $this->_hydrate();
        if (isset($this->getVars()[$offset])) {
            if (is_resource($this->getVars()[$offset])) {
                return stream_get_contents($this->getVars()[$offset]);
            }
            return $this->getVars()[$offset];
        }

        if (isset($this->meta[$offset])) {
            if (is_resource($this->meta[$offset])) {
                return stream_get_contents($this->meta[$offset]);
            }
            return $this->meta[$offset];
        }

        if ($this->magicMethodExist($offset)) {
            return $this->callMagicFunction($offset);
        }

        return null;
    }

    public function offsetSet($offset, $value): void
    {
        $this->meta[$offset] = $value;
//        $this->{$offset} = $value;
    }

    public function offsetUnset($offset): void
    {
        unset($this->meta[$offset]);
    }

    public function magicId()
    {
        return base64_encode(json_encode([
            "c" => get_class($this),
            "id" => $this->getIdField()
        ]));
    }

    public static function fromMagicId(string $magicId)
    {
        $a = json_decode(base64_decode($magicId), true);
        $className = $a['c'];
        $uuid = $a['id'];

        if (class_exists($className)) {
            $obj = new $className();
            if ($obj instanceof DataModel) {
                return $obj::get($uuid);
            }
        }

        return null;
    }

//    public function __set($name, $value)
//    {
//        if (!isset($this->{$name})) {
//            $this->meta[$name] = $value;
//        }
//    }

//    public function __construct()
//    {
//        $this->_hydrate();
//    }
}