<?php
// Copyright (c) Pickware GmbH. All rights reserved.
// This file is part of software that is released under a proprietary license.
// You must not copy, modify, distribute, make publicly available, or execute
// its contents or parts thereof without express permission by the copyright
// holder, unless otherwise permitted by law.

namespace Shopware\Plugins\ViisonCommon\Classes\Installation;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Shopware\Models\Config\Element;
use Doctrine\ORM\EntityManager;

class Lock
{

    /**
     * @var Connection
     */
    private $connection;

    /**
     * @var EntityManager
     */
    private $entityManager;

    /**
     * @var string
     */
    private $lockIdentifier;

    /**
     * @var string
     */
    private $lockDescription;

    /**
     * @var boolean whether the lock is being held (has been acquired)
     */
    private $hasBeenAcquired = false;

    /**
     * @todo Change to \Shopware\Models\Config\Form::class once we can depend on PHP >= 5.5.
     */
    const FORM_CLASS = 'Shopware\\Models\\Config\\Form';

    /**
     * @todo Change to \Shopware\Models\Config\Element::class once we can depend on PHP >= 5.5.
     */
    const ELEMENT_CLASS = 'Shopware\\Models\\Config\\Element';

    /**
     * @param string $lockIdentifier the constant, unique name of the lock
     * @param string $lockDescription
     * @param EntityManager $entityManager an
     */
    public function __construct($lockIdentifier, $lockDescription, EntityManager $entityManager)
    {
        $this->lockIdentifier = $lockIdentifier;
        $this->lockDescription = $lockDescription;
        $this->entityManager = $this->initNewEntityManager($entityManager);
        $this->connection = $entityManager->getConnection();
    }

    private function initNewEntityManager($templateEntityManager)
    {
        return $templateEntityManager->create(
            $templateEntityManager->getConnection(),
            $templateEntityManager->getConfiguration(),
            $templateEntityManager->getEventManager()
        );
    }

    protected function resetEntityManager()
    {
        $this->entityManager = $this->initNewEntityManager($this->entityManager);
    }

    /**
     * @return LockStatus
     * @throws DBALException
     */
    public function tryAcquireLock()
    {
        $lock = new Element($this->getLockType(), $this->lockIdentifier, []);
        $lock->setValue($this->getLockValue());
        $lock->setDescription($this->lockDescription);

        /* Config elements need a non-null form_id. However, the reference is not actually checked.
         * There is no Form with id=0 in a default Shopware install, but there are many config elements referencing
         * form_id=0. */
        $lock->setForm($this->getFakeForm());

        try {
            $this->entityManager->persist($lock);
            $this->entityManager->flush($lock);

            return $this->applyAndReturnLockStatus($this->createLockStatus(true, $lock));
        } catch (DBALException $e) {
            if (!$this->entityManager->isOpen()) {
                /* An exception may have closed the EntityManager. As this was an expected exception, we want to
                 * re-open it. Otherwise, all further attempty to use the EntityManager will fail. */
                $this->resetEntityManager();
            }

            $existingLock = $this->fetchLock();

            $lockStatus = null;
            // Rethrow the exception only if the cause is not an attempt to
            // insert the lock while one already exists, which is expected
            // behavior:
            if ($existingLock === null) {
                $previous = $e->getPrevious();
                if (!$previous || !($previous instanceof \PDOException) || $previous->getCode() !== '23000') {
                    // If the code of the previous exception is not exactly "23000" (duplicate entry), the failure is not
                    // caused by trying to replace an existing lock.
                    // The error code documented is documented here: <https://dev.mysql.com/doc/refman/5.5/en/server-error-reference.html#error_er_dup_entry>
                    // We need to check the previous PDOException because the DBALException does not retain the
                    // SQLSTATE error code.
                    throw $e;
                }

                // Even if the exception is a "duplicate entry" error, the entity $existingLock may not exist. This can
                // happen because of race conditions between simultaneous requests.
                $lockStatus = new LockStatus(false, $this->lockDescription);
            } else {
                $lockStatus = $this->handleExistingLock($existingLock);
            }

            return $this->applyAndReturnLockStatus($lockStatus);
        }
    }

    private function applyAndReturnLockStatus(LockStatus $status)
    {
        $this->hasBeenAcquired = $status->hasBeenAcquired();

        return $status;
    }

    private function getFakeForm()
    {
        return $this->entityManager->getPartialReference(static::FORM_CLASS, 0);
    }

    /**
     * @return Element|null
     */
    protected function fetchLock()
    {
        return $this->entityManager->getRepository(static::ELEMENT_CLASS)->findOneBy([
            'name' => $this->lockIdentifier,
            'form' => $this->getFakeForm(),
        ]);
    }

    protected function getLockType()
    {
        return 'number';
    }

    protected function getLockValue()
    {
        return 0;
    }

    protected function handleExistingLock(Element $existingLock)
    {
        return $this->createLockStatus(false, $existingLock);
    }

    /**
     * @param boolean $hasBeenAcquired
     */
    protected function createLockStatus($hasBeenAcquired, Element $lock)
    {
        //$description = ($existingLock === null) ? $this->lockDescription : $existingLock->getDescription();
        return new LockStatus($hasBeenAcquired, $lock->getDescription());
    }

    protected function isAcquired()
    {
        return $this->hasBeenAcquired;
    }

    final protected function getEntityManager()
    {
        return $this->entityManager;
    }

    /**
     * Releases the lock if it has previously been acquired by this Lock instance.
     *
     * @return void
     */
    public function releaseOnlyIfAlreadyHeld()
    {
        if (!$this->isAcquired()) {
            return;
        }

        $this->releaseUnconditionally();
    }

    /**
     * Delete any occurrence of this lock.
     *
     * @return void
     */
    public function releaseUnconditionally()
    {
        if (!$this->entityManager->isOpen()) {
            $this->resetEntityManager();
        }

        $element = $this->fetchLock();

        if ($element === null) {
            return;
        }

        $this->entityManager->remove($element);
        $this->entityManager->flush();
    }
}
