<?php

namespace SystemConnector\BacklogService\Storage;

use DateTime;
use Doctrine\DBAL\Connection;
use PDO;
use SystemConnector\BacklogService\BacklogService;
use SystemConnector\BacklogService\Command\HandleBacklogElementCommand;
use SystemConnector\ServiceBus\Command\CommandInterface;

class DatabaseBacklogServiceStorage implements BacklogServiceStorageInterface
{
    /**
     * @var Connection
     */
    private $connection;

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

    public function __construct(
        Connection $connection,
        $table
    ) {
        $this->connection = $connection;
        $this->table = $table;
    }

    /**
     * {@inheritdoc}
     */
    public function enqueue(CommandInterface $command)
    {
        $serializedCommand = serialize($command);
        $hash = md5($serializedCommand);

        if ($this->entryExists($hash)) {
            return;
        }

        $this->connection->insert($this->table, [
            'payload' => $serializedCommand,
            'hash' => $hash,
            'priority' => $command->getPriority(),
            'time' => (new DateTime('now'))->format(DATE_W3C),
            'status' => BacklogService::STATUS_OPEN,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function dequeue()
    {
        $queryBuilder = $this->connection->createQueryBuilder();
        $queryBuilder->from($this->table, 'backlog');
        $queryBuilder->andWhere('backlog.status = :status');
        $queryBuilder->setParameter(':status', BacklogService::STATUS_OPEN);
        $queryBuilder->addOrderBy('priority', 'DESC');
        $queryBuilder->addOrderBy('id', 'ASC');
        $queryBuilder->setMaxResults(1);
        $queryBuilder->select('*');

        $backlog = $queryBuilder->execute()->fetch(PDO::FETCH_ASSOC);

        if (empty($backlog)) {
            return null;
        }

        $affectedRows = $this->connection->update(
            $this->table,
            [
                'status' => BacklogService::STATUS_PROCESSED,
            ],
            [
                'id' => $backlog['id'],
            ]
        );

        if ($affectedRows !== 1) {
            return null;
        }

        $affectedRows = $this->connection->delete($this->table, [
            'id' => $backlog['id'],
        ]);

        if ($affectedRows !== 1) {
            return null;
        }

        $command = unserialize($backlog['payload'], [
            'allowed_classes' => true, ]
        );

        if (!($command instanceof CommandInterface)) {
            return null;
        }

        return new HandleBacklogElementCommand($command);
    }

    /**
     * {@inheritdoc}
     */
    public function getInfo()
    {
        $amount = $this->getEnqueuedAmount();

        return [
            'amount_enqueued' => $amount,
        ];
    }

    /**
     * @param string $hash
     *
     * @return bool
     */
    private function entryExists($hash)
    {
        $queryBuilder = $this->connection->createQueryBuilder();
        $queryBuilder->from($this->table, 'backlog');
        $queryBuilder->andWhere('backlog.hash = :hash');
        $queryBuilder->setParameter(':hash', $hash);
        $queryBuilder->setMaxResults(1);
        $queryBuilder->select('backlog.id');

        $backlog = $queryBuilder->execute()->fetch(PDO::FETCH_ASSOC);

        if (!empty($backlog)) {
            return true;
        }

        return false;
    }

    /**
     * @return int
     */
    private function getEnqueuedAmount()
    {
        $queryBuilder = $this->connection->createQueryBuilder();
        $queryBuilder->from($this->table, 'backlog');
        $queryBuilder->select('count(backlog.id) as count');

        return $queryBuilder->execute()->fetch(PDO::FETCH_COLUMN);
    }
}


<?php

namespace ShopwareAdapter\ServiceBus\QueryHandler\Payment;

use ShopwareAdapter\DataProvider\Order\OrderDataProviderInterface;
use ShopwareAdapter\ResponseParser\Payment\PaymentResponseParserInterface;
use ShopwareAdapter\ShopwareAdapter;
use SystemConnector\IdentityService\IdentityServiceInterface;
use SystemConnector\ServiceBus\Query\FetchTransferObjectQuery;
use SystemConnector\ServiceBus\Query\QueryInterface;
use SystemConnector\ServiceBus\QueryHandler\QueryHandlerInterface;
use SystemConnector\ServiceBus\QueryType;
use SystemConnector\TransferObject\Payment\Payment;

class FetchPaymentQueryHandler implements QueryHandlerInterface
{
    /**
     * @var PaymentResponseParserInterface
     */
    private $responseParser;

    /**
     * @var IdentityServiceInterface
     */
    private $identityService;

    /**
     * @var OrderDataProviderInterface
     */
    private $dataProvider;

    public function __construct(
        PaymentResponseParserInterface $responseParser,
        IdentityServiceInterface $identityService,
        OrderDataProviderInterface $dataProvider
    ) {
        $this->responseParser = $responseParser;
        $this->identityService = $identityService;
        $this->dataProvider = $dataProvider;
    }

    /**
     * {@inheritdoc}
     */
    public function supports(QueryInterface $query)
    {
        return $query instanceof FetchTransferObjectQuery &&
            $query->getAdapterName() === ShopwareAdapter::NAME &&
            $query->getObjectType() === Payment::TYPE &&
            $query->getQueryType() === QueryType::ONE;
    }

    /**
     * {@inheritdoc}
     *
     * @param FetchTransferObjectQuery $query
     */
    public function handle(QueryInterface $query)
    {
        $identity = $this->identityService->findOneBy([
            'objectIdentifier' => $query->getObjectIdentifier(),
            'objectType' => Payment::TYPE,
            'adapterName' => ShopwareAdapter::NAME,
        ]);

        if (null === $identity) {
            return [];
        }

        $order = $this->dataProvider->getOrderDetails($identity->getAdapterIdentifier());
        $order = $this->responseParser->parse($order);

        return array_filter($order);
    }
}


<?php

namespace PlentyConnector\Components\Bundle\ShopwareAdapter\ResponseParser\Order;

use ShopwareAdapter\ResponseParser\Order\OrderResponseParserInterface;

class OrderResponseParser implements OrderResponseParserInterface
{
    /**
     * @var OrderResponseParserInterface
     */
    private $parentOrderResponseParser;

    public function __construct(OrderResponseParserInterface $parentOrderResponseParser)
    {
        $this->parentOrderResponseParser = $parentOrderResponseParser;
    }

    /**
     * {@inheritdoc}
     */
    public function parse(array $entry)
    {
        foreach ($entry['details'] as $key => $item) {
            if (!isset($item['attribute']['bundlePackageId'])) {
                continue;
            }

            if ($item['mode'] !== 10) {
                unset($entry['details'][$key]);

                continue;
            }

            $entry['details'][$key]['bundle'] = 1;
        }

        return $this->parentOrderResponseParser->parse($entry);
    }
}


<?php

namespace ShopwareAdapter\ServiceBus\CommandHandler\Stock;

use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shopware\Models\Article\Detail;
use ShopwareAdapter\ShopwareAdapter;
use SystemConnector\IdentityService\IdentityServiceInterface;
use SystemConnector\ServiceBus\Command\CommandInterface;
use SystemConnector\ServiceBus\Command\TransferObjectCommand;
use SystemConnector\ServiceBus\CommandHandler\CommandHandlerInterface;
use SystemConnector\ServiceBus\CommandType;
use SystemConnector\TransferObject\Product\Stock\Stock;
use SystemConnector\TransferObject\Product\Variation\Variation;

class HandleStockCommandHandler implements CommandHandlerInterface
{
    /**
     * @var IdentityServiceInterface
     */
    private $identityService;

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

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(
        IdentityServiceInterface $identityService,
        EntityManagerInterface $entityManager,
        LoggerInterface $logger
    ) {
        $this->identityService = $identityService;
        $this->entityManager = $entityManager;
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function supports(CommandInterface $command)
    {
        return $command instanceof TransferObjectCommand &&
            $command->getAdapterName() === ShopwareAdapter::NAME &&
            $command->getObjectType() === Stock::TYPE &&
            $command->getCommandType() === CommandType::HANDLE;
    }

    /**
     * {@inheritdoc}
     *
     * @param TransferObjectCommand $command
     */
    public function handle(CommandInterface $command)
    {
        /**
         * @var Stock $stock
         */
        $stock = $command->getPayload();

        $variationIdentity = $this->identityService->findOneBy([
            'objectIdentifier' => $stock->getVariationIdentifier(),
            'objectType' => Variation::TYPE,
            'adapterName' => ShopwareAdapter::NAME,
        ]);

        if (null === $variationIdentity) {
            $this->logger->notice('could not find variation identity - ' . $stock->getVariationIdentifier());

            return false;
        }

        $this->identityService->findOneOrCreate(
            $variationIdentity->getAdapterIdentifier(),
            ShopwareAdapter::NAME,
            Stock::TYPE
        );

        $variationRespository = $this->entityManager->getRepository(Detail::class);

        /**
         * @var null|Detail $variation
         */
        $variation = $variationRespository->find($variationIdentity->getAdapterIdentifier());

        if (null === $variation) {
            $this->logger->notice('could not find variation - ' . $stock->getVariationIdentifier());

            return false;
        }

        $variation->setInStock($stock->getStock());

        $this->entityManager->persist($variation);
        $this->entityManager->flush();
        $this->entityManager->clear();

        return true;
    }
}


<?php

namespace PlentymarketsAdapter\ServiceBus\CommandHandler\Payment;

use PlentymarketsAdapter\Client\ClientInterface;
use PlentymarketsAdapter\PlentymarketsAdapter;
use PlentymarketsAdapter\RequestGenerator\Payment\PaymentRequestGeneratorInterface;
use Psr\Log\LoggerInterface;
use SystemConnector\IdentityService\IdentityServiceInterface;
use SystemConnector\ServiceBus\Command\CommandInterface;
use SystemConnector\ServiceBus\Command\TransferObjectCommand;
use SystemConnector\ServiceBus\CommandHandler\CommandHandlerInterface;
use SystemConnector\ServiceBus\CommandType;
use SystemConnector\TransferObject\Order\Order;
use SystemConnector\TransferObject\Payment\Payment;

class HandlePaymentCommandHandler implements CommandHandlerInterface
{
    /**
     * @var ClientInterface
     */
    private $client;

    /**
     * @var IdentityServiceInterface
     */
    private $identityService;

    /**
     * @var PaymentRequestGeneratorInterface
     */
    private $requestGenerator;

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(
        ClientInterface $client,
        IdentityServiceInterface $identityService,
        PaymentRequestGeneratorInterface $requestGenerator,
        LoggerInterface $logger
    ) {
        $this->client = $client;
        $this->identityService = $identityService;
        $this->requestGenerator = $requestGenerator;
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function supports(CommandInterface $command)
    {
        return $command instanceof TransferObjectCommand &&
            $command->getAdapterName() === PlentymarketsAdapter::NAME &&
            $command->getObjectType() === Payment::TYPE &&
            $command->getCommandType() === CommandType::HANDLE;
    }

    /**
     * {@inheritdoc}
     *
     * @var TransferObjectCommand $command
     */
    public function handle(CommandInterface $command)
    {
        /**
         * @var Payment $payment
         */
        $payment = $command->getPayload();

        $identity = $this->identityService->findOneBy([
            'objectIdentifier' => $payment->getIdentifier(),
            'objectType' => Payment::TYPE,
            'adapterName' => PlentymarketsAdapter::NAME,
        ]);

        if (null !== $identity) {
            return true;
        }

        $orderIdentity = $this->identityService->findOneBy([
            'objectIdentifier' => $payment->getOrderIdentifer(),
            'objectType' => Order::TYPE,
            'adapterName' => PlentymarketsAdapter::NAME,
        ]);

        if (null === $orderIdentity) {
            $this->logger->notice('order was not exported before payment handling');

            return false;
        }

        $paymentResult = $this->findOrCreatePlentyPayment($payment);

        if ((int) $orderIdentity->getAdapterIdentifier() === (int) $paymentResult['order']['orderId']) {
            return true;
        }

        $this->client->request(
            'POST',
            'payment/' . $paymentResult['id'] . '/order/' . $orderIdentity->getAdapterIdentifier()
        );

        return true;
    }

    /**
     * @param Payment $payment
     *
     * @return array
     */
    private function findOrCreatePlentyPayment(Payment $payment)
    {
        $plentyPayments = $this->fetchPlentyPayments($payment);

        if ($plentyPayments) {
            $paymentResult = $plentyPayments[0];
            $this->logger->debug('payment with the same transaction id "' . $paymentResult['id'] . '" already exists.');
        } else {
            $paymentResult = $this->createPlentyPayment($payment);
        }

        $this->identityService->insert(
            $payment->getIdentifier(),
            Payment::TYPE,
            (string) $paymentResult['id'],
            PlentymarketsAdapter::NAME
        );

        return $paymentResult;
    }

    /**
     * @param Payment $payment
     *
     * @return bool
     */
    private function fetchPlentyPayments($payment)
    {
        $url = 'payments/property/1/' . $payment->getTransactionReference();
        $payments = $this->client->request('GET', $url);

        if (empty($payments)) {
            return false;
        }

        $payments = array_filter($payments, function (array $payment) {
            return !$payment['deleted'];
        });

        return $payments;
    }

    /**
     * @param Payment $payment
     *
     * @return array
     */
    private function createPlentyPayment(Payment $payment)
    {
        $params = $this->requestGenerator->generate($payment);

        return $this->client->request('POST', 'payments', $params);
    }
}


<?php

namespace SystemConnector\TransferObject\Product\Price;

use SystemConnector\ValueObject\AbstractValueObject;

class Price extends AbstractValueObject
{
    /**
     * @var float
     */
    private $price = 0.0;

    /**
     * @var float
     */
    private $pseudoPrice = 0.0;

    /**
     * @var null|string
     */
    private $customerGroupIdentifier;

    /**
     * @var float
     */
    private $fromAmount = 1.0;

    /**
     * @var null|float
     */
    private $toAmount;

    /**
     * @return float
     */
    public function getPrice()
    {
        return $this->price;
    }

    /**
     * @param float $price
     */
    public function setPrice($price)
    {
        $this->price = $price;
    }

    /**
     * @return float
     */
    public function getPseudoPrice()
    {
        return $this->pseudoPrice;
    }

    /**
     * @param float $pseudoPrice
     */
    public function setPseudoPrice($pseudoPrice)
    {
        $this->pseudoPrice = $pseudoPrice;
    }

    /**
     * @return null|string
     */
    public function getCustomerGroupIdentifier()
    {
        return $this->customerGroupIdentifier;
    }

    /**
     * @param null|string $customerGroupIdentifier
     */
    public function setCustomerGroupIdentifier($customerGroupIdentifier = null)
    {
        $this->customerGroupIdentifier = $customerGroupIdentifier;
    }

    /**
     * @return float
     */
    public function getFromAmount()
    {
        return $this->fromAmount;
    }

    /**
     * @param float $fromAmount
     */
    public function setFromAmount($fromAmount)
    {
        $this->fromAmount = $fromAmount;
    }

    /**
     * @return float
     */
    public function getToAmount()
    {
        return $this->toAmount;
    }

    /**
     * @param null|float $toAmount
     */
    public function setToAmount($toAmount = null)
    {
        $this->toAmount = $toAmount;
    }

    /**
     * {@inheritdoc}
     */
    public function getClassProperties()
    {
        return [
            'price' => $this->getPrice(),
            'pseudoPrice' => $this->getPseudoPrice(),
            'customerGroupIdentifier' => $this->getCustomerGroupIdentifier(),
            'fromAmount' => $this->getFromAmount(),
            'toAmount' => $this->getToAmount(),
        ];
    }
}


<?php

namespace SystemConnector\Console\Command;

use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use SystemConnector\CleanupService\CleanupServiceInterface;
use SystemConnector\Console\OutputHandler\OutputHandlerInterface;
use SystemConnector\Logger\ConsoleHandler;
use Throwable;

class CleanupCommand extends Command
{
    /**
     * @var CleanupServiceInterface
     */
    private $cleanupService;

    /**
     * @var OutputHandlerInterface
     */
    private $outputHandler;

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(
        CleanupServiceInterface $cleanupService,
        OutputHandlerInterface $outputHandler,
        LoggerInterface $logger
    ) {
        $this->cleanupService = $cleanupService;
        $this->outputHandler = $outputHandler;
        $this->logger = $logger;

        parent::__construct();
    }

    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        $this->setName('plentyconnector:cleanup');
        $this->setDescription('remove orphaned transfer objects');
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (method_exists($this->logger, 'pushHandler')) {
            $this->logger->pushHandler(new ConsoleHandler($output));
        }

        $this->outputHandler->initialize($input, $output);

        try {
            $this->cleanupService->cleanup();
        } catch (Exception $exception) {
            $this->logger->error($exception->getMessage());
        } catch (Throwable $exception) {
            $this->logger->error($exception->getMessage());
        }
    }
}


<?php

namespace PlentyConnector\Components\PayPal\Plentymarkets;

use PlentyConnector\Components\PayPal\PaymentData\PayPalUnifiedInstallmentPaymentData;
use PlentymarketsAdapter\RequestGenerator\Payment\PaymentRequestGeneratorInterface;
use SystemConnector\TransferObject\Payment\Payment;

class PayPalUnifiedInstallmentRequestGenerator implements PaymentRequestGeneratorInterface
{
    /**
     * @var PaymentRequestGeneratorInterface
     */
    private $parentRequestGenerator;

    public function __construct(PaymentRequestGeneratorInterface $parentRequestGenerator)
    {
        $this->parentRequestGenerator = $parentRequestGenerator;
    }

    /**
     * {@inheritdoc}
     */
    public function generate(Payment $payment)
    {
        $paymentParams = $this->parentRequestGenerator->generate($payment);
        $data = $payment->getPaymentData();

        if (!($data instanceof PayPalUnifiedInstallmentPaymentData)) {
            return $paymentParams;
        }

        $paymentParams['properties'][] = [
            'typeId' => 22,
            'value' => json_encode([
                'financingCosts' => $data->getFeeAmount(),
                'totalCostsIncludeFinancing' => $data->getTotalCost(),
                'monthlyPayment' => $data->getMonthlyPayment(),
                'term' => $data->getTerm(),
            ]),
        ];

        return $paymentParams;
    }
}


<?php

namespace ShopwareAdapter\RequestGenerator\Product;

use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Shopware\Models\Article\Article;
use Shopware\Models\Category\Category as CategoryModel;
use Shopware\Models\Category\Repository as CategoryRepository;
use Shopware\Models\Property\Group as GroupModel;
use Shopware\Models\Shop\Repository as ShopRepository;
use Shopware\Models\Shop\Shop as ShopModel;
use ShopwareAdapter\RequestGenerator\Product\ConfiguratorSet\ConfiguratorSetRequestGeneratorInterface;
use ShopwareAdapter\ShopwareAdapter;
use SystemConnector\ConfigService\ConfigServiceInterface;
use SystemConnector\IdentityService\IdentityServiceInterface;
use SystemConnector\TransferObject\Category\Category;
use SystemConnector\TransferObject\Manufacturer\Manufacturer;
use SystemConnector\TransferObject\Media\Media;
use SystemConnector\TransferObject\Product\Badge\Badge;
use SystemConnector\TransferObject\Product\LinkedProduct\LinkedProduct;
use SystemConnector\TransferObject\Product\Product;
use SystemConnector\TransferObject\ShippingProfile\ShippingProfile;
use SystemConnector\TransferObject\Shop\Shop;
use SystemConnector\TransferObject\VatRate\VatRate;
use SystemConnector\ValueObject\Attribute\Attribute;

class ProductRequestGenerator implements ProductRequestGeneratorInterface
{
    /**
     * @var IdentityServiceInterface
     */
    private $identityService;

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

    /**
     * @var ConfiguratorSetRequestGeneratorInterface
     */
    private $configuratorSetRequestGenerator;

    /**
     * @var ConfigServiceInterface
     */
    private $configService;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @var array
     */
    private $categories;

    public function __construct(
        IdentityServiceInterface $identityService,
        EntityManagerInterface $entityManager,
        ConfiguratorSetRequestGeneratorInterface $configuratorSetRequestGenerator,
        ConfigServiceInterface $configService,
        LoggerInterface $logger
    ) {
        $this->identityService = $identityService;
        $this->entityManager = $entityManager;
        $this->configuratorSetRequestGenerator = $configuratorSetRequestGenerator;
        $this->configService = $configService;
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function generate(Product $product)
    {
        $this->addShippingProfilesAsAttributes($product);

        $shopIdentifiers = array_filter($product->getShopIdentifiers(), function ($identifier) {
            $shopIdentity = $this->identityService->findOneBy([
                'objectIdentifier' => $identifier,
                'objectType' => Shop::TYPE,
                'adapterName' => ShopwareAdapter::NAME,
            ]);

            return null !== $shopIdentity;
        });

        if (empty($shopIdentifiers)) {
            return [];
        }

        $vatIdentity = $this->identityService->findOneBy([
            'objectIdentifier' => $product->getVatRateIdentifier(),
            'objectType' => VatRate::TYPE,
            'adapterName' => ShopwareAdapter::NAME,
        ]);

        if (null === $vatIdentity) {
            throw new InvalidArgumentException('vat rate not mapped - ' . $product->getVatRateIdentifier());
        }

        $manufacturerIdentity = $this->identityService->findOneBy([
            'adapterName' => ShopwareAdapter::NAME,
            'objectType' => Manufacturer::TYPE,
            'objectIdentifier' => $product->getManufacturerIdentifier(),
        ]);

        if (null === $manufacturerIdentity) {
            throw new InvalidArgumentException('manufacturer is missing - ' . $product->getManufacturerIdentifier());
        }

        $propertyData = $this->getPropertyData($product);

        $params = [
            'filterGroupId' => $propertyData['filterGroupId'],
            'propertyValues' => $propertyData['propertyValues'],
            'mainDetail' => [
                'number' => $product->getNumber(),
            ],
            'availableFrom' => $product->getAvailableFrom(),
            'availableTo' => $product->getAvailableTo(),
            'name' => $product->getName(),
            'description' => $product->getMetaDescription(),
            'descriptionLong' => !empty($product->getLongDescription()) ? $product->getLongDescription() : $product->getDescription(),
            'categories' => $this->getCategories($product),
            'seoCategories' => $this->getSeoCategories($product),
            'taxId' => $vatIdentity->getAdapterIdentifier(),
            'notification' => $this->configService->get('item_notification') === 'true' ? 1 : 0,
            'active' => $product->isActive(),
            'highlight' => $this->getHighlightFlag($product),
            'images' => $this->getImages($product),
            'similar' => $this->getLinkedProducts($product),
            'related' => $this->getLinkedProducts($product, LinkedProduct::TYPE_ACCESSORY),
            'metaTitle' => $product->getMetaTitle(),
            'keywords' => $product->getMetaKeywords(),
            'changed' => (new DateTime('now'))->format('Y-m-d H:i:s'),
            'supplierId' => $manufacturerIdentity->getAdapterIdentifier(),
            '__options_categories' => ['replace' => true],
            '__options_seoCategories' => ['replace' => true],
            '__options_similar' => ['replace' => true],
            '__options_related' => ['replace' => true],
            '__options_images' => ['replace' => true],
        ];

        $configuratorSet = $this->configuratorSetRequestGenerator->generate($product);

        if (!empty($configuratorSet)) {
            $params['configuratorSet'] = $configuratorSet;
        }

        return $params;
    }

    /**
     * @param Product $product
     */
    private function addShippingProfilesAsAttributes(Product $product)
    {
        $allProfileIdentities = $this->identityService->findBy([
            'objectType' => ShippingProfile::TYPE,
            'adapterName' => ShopwareAdapter::NAME,
        ]);

        $shippingAttributes = [];
        foreach ($allProfileIdentities as $identity) {
            $shippingAttributes['shippingProfile' . $identity->getAdapterIdentifier()] = '';
        }

        $attributes = $product->getAttributes();

        foreach ($product->getShippingProfileIdentifiers() as $identifier) {
            $profileIdentity = $this->identityService->findOneBy([
                'objectIdentifier' => $identifier,
                'objectType' => ShippingProfile::TYPE,
                'adapterName' => ShopwareAdapter::NAME,
            ]);

            if (null === $profileIdentity) {
                continue;
            }

            $existingAttributes = array_filter($attributes, function (Attribute $attribute) use ($profileIdentity) {
                return $attribute->getKey() === 'shippingProfile' . $profileIdentity->getAdapterIdentifier();
            });

            if (!empty($existingAttributes)) {
                $this->logger->notice('shippingProfile is not a allowed attribute key');

                continue;
            }

            $shippingAttributes['shippingProfile' . $profileIdentity->getAdapterIdentifier()] = $profileIdentity->getObjectIdentifier();
        }

        foreach ($shippingAttributes as $key => $value) {
            $attributes[] = Attribute::fromArray([
                'key' => $key,
                'value' => $value,
            ]);
        }

        $product->setAttributes($attributes);
    }

    /**
     * @param Product $product
     * @param string  $type
     *
     * @return array
     */
    private function getLinkedProducts(Product $product, $type = LinkedProduct::TYPE_SIMILAR)
    {
        $result = [];

        foreach ($product->getLinkedProducts() as $linkedProduct) {
            if ($linkedProduct->getType() === $type) {
                $productIdentity = $this->identityService->findOneBy([
                    'objectIdentifier' => $linkedProduct->getProductIdentifier(),
                    'objectType' => Product::TYPE,
                    'adapterName' => ShopwareAdapter::NAME,
                ]);

                if (null === $productIdentity) {
                    continue;
                }

                /**
                 * @var EntityRepository $productRepository
                 */
                $productRepository = $this->entityManager->getRepository(Article::class);

                $productExists = $productRepository->find($productIdentity->getAdapterIdentifier());

                if (!$productExists) {
                    continue;
                }

                $result[$productIdentity->getAdapterIdentifier()] = [
                    'id' => $productIdentity->getAdapterIdentifier(),
                    'number' => null,
                    'position' => $linkedProduct->getPosition(),
                    'cross' => false,
                ];
            }
        }

        return $result;
    }

    /**
     * @param Product $product
     *
     * @return array
     */
    private function getPropertyData(Product $product)
    {
        /**
         * @var EntityRepository $groupRepository
         */
        $groupRepository = $this->entityManager->getRepository(GroupModel::class);

        /**
         * @var GroupModel $propertyGroup
         */
        $propertyGroup = $groupRepository->findOneBy(['name' => 'PlentyConnector']);

        if (null === $propertyGroup) {
            $propertyGroup = new GroupModel();
            $propertyGroup->setName('PlentyConnector');
            $propertyGroup->setPosition(1);
            $propertyGroup->setComparable(true);
            $propertyGroup->setSortMode(1);

            $this->entityManager->persist($propertyGroup);
            $this->entityManager->flush();
            $this->entityManager->clear();
        }

        $result = [];
        $result['filterGroupId'] = $propertyGroup->getId();
        $result['propertyValues'] = [];

        foreach ($product->getProperties() as $property) {
            foreach ($property->getValues() as $value) {
                $result['propertyValues'][] = [
                    'option' => [
                        'name' => $property->getName(),
                    ],
                    'value' => $value->getValue(),
                ];
            }
        }

        return $result;
    }

    /**
     * @param Product $product
     *
     * @return array|bool
     */
    private function getImages(Product $product)
    {
        $images = [];
        foreach ($product->getImages() as $image) {
            $shopIdentifiers = array_filter($image->getShopIdentifiers(), function ($shop) {
                $identity = $this->identityService->findOneBy([
                    'objectIdentifier' => (string) $shop,
                    'objectType' => Shop::TYPE,
                    'adapterName' => ShopwareAdapter::NAME,
                ]);

                return $identity !== null;
            });

            if (empty($shopIdentifiers)) {
                continue;
            }

            $imageIdentity = $this->identityService->findOneBy([
                'objectIdentifier' => $image->getMediaIdentifier(),
                'objectType' => Media::TYPE,
                'adapterName' => ShopwareAdapter::NAME,
            ]);

            if (null === $imageIdentity) {
                $this->logger->notice('image is missing - ' . $image->getMediaIdentifier());

                return false;
            }

            $images[] = [
                'mediaId' => $imageIdentity->getAdapterIdentifier(),
                'position' => $image->getPosition(),
            ];
        }

        return $images;
    }

    /**
     * @param Product $product
     *
     * @return array
     */
    private function getCategories(Product $product)
    {
        /**
         * @var CategoryRepository $categoryRepository
         */
        $categoryRepository = $this->entityManager->getRepository(CategoryModel::class);

        /**
         * @var ShopRepository $shopRepository
         */
        $shopRepository = $this->entityManager->getRepository(ShopModel::class);

        $shopCategories = [];

        foreach ($product->getShopIdentifiers() as $shopIdentifier) {
            $identities = $this->identityService->findBy([
                    'objectIdentifier' => (string) $shopIdentifier,
                    'objectType' => Shop::TYPE,
                    'adapterName' => ShopwareAdapter::NAME,
            ]);

            if ($identities === null) {
                continue;
            }

            foreach ($identities as $identity) {
                /**
                 * @var ShopModel $shop
                 */
                $shop = $shopRepository->find($identity->getAdapterIdentifier());

                if ($shop !== null) {
                    $shopCategories[] = $shop->getCategory()->getId();
                }
            }
        }

        $this->categories = [];

        foreach ($product->getCategoryIdentifiers() as $categoryIdentifier) {
            $categoryIdentities = $this->identityService->findBy([
                'objectIdentifier' => $categoryIdentifier,
                'objectType' => Category::TYPE,
                'adapterName' => ShopwareAdapter::NAME,
            ]);

            foreach ($categoryIdentities as $categoryIdentity) {
                if (in_array($categoryIdentity->getAdapterIdentifier(), array_column($this->categories, 'id'), true)) {
                    continue;
                }

                $category = $categoryRepository->find($categoryIdentity->getAdapterIdentifier());

                if (null === $category) {
                    continue;
                }

                $extractedCategoryPath = array_filter(explode('|', $category->getPath()));
                $matchedShopCategories = array_intersect($extractedCategoryPath, $shopCategories);

                if (empty($matchedShopCategories)) {
                    continue;
                }

                $this->categories[] = [
                    'id' => $categoryIdentity->getAdapterIdentifier(),
                ];
            }
        }

        return $this->categories;
    }

    private function getSeoCategories(Product $product)
    {
        /**
         * @var CategoryRepository $categoryRepository
         */
        $categoryRepository = $this->entityManager->getRepository(CategoryModel::class);

        /**
         * @var ShopRepository $shopRepository
         */
        $shopRepository = $this->entityManager->getRepository(ShopModel::class);

        $seoCategories = [];
        foreach ($product->getDefaultCategoryIdentifiers() as $categoryIdentifier) {
            $categoryIdentities = $this->identityService->findBy([
                'objectIdentifier' => $categoryIdentifier,
                'objectType' => Category::TYPE,
                'adapterName' => ShopwareAdapter::NAME,
            ]);

            foreach ($categoryIdentities as $categoryIdentity) {
                if (!in_array($categoryIdentity->getAdapterIdentifier(), array_column($this->categories, 'id'), true)) {
                    continue;
                }

                /**
                 * @var null|CategoryModel $category
                 */
                $category = $categoryRepository->find($categoryIdentity->getAdapterIdentifier());

                if (null === $category) {
                    continue;
                }

                $parents = array_reverse(array_filter(explode('|', $category->getPath())));

                /**
                 * @var ShopModel[] $shops
                 */
                $shops = $shopRepository->findBy([
                    'categoryId' => array_shift($parents),
                ]);

                foreach ($shops as $shop) {
                    if (in_array($shop->getId(), array_column($seoCategories, 'shopId'), true)) {
                        continue;
                    }

                    $seoCategories[] = [
                        'categoryId' => $categoryIdentity->getAdapterIdentifier(),
                        'shopId' => $shop->getId(),
                    ];
                }
            }
        }

        return $seoCategories;
    }

    /**
     * @param Product $product
     *
     * @return int
     */
    private function getHighlightFlag(Product $product)
    {
        foreach ($product->getBadges() as $badge) {
            if ($badge->getType() === Badge::TYPE_HIGHLIGHT) {
                return 1;
            }
        }

        return 0;
    }
}


<?php

namespace ShopwareAdapter\ServiceBus\CommandHandler\Stock;

use Psr\Log\LoggerInterface;
use ShopwareAdapter\ShopwareAdapter;
use SystemConnector\IdentityService\IdentityServiceInterface;
use SystemConnector\IdentityService\Struct\Identity;
use SystemConnector\ServiceBus\Command\CommandInterface;
use SystemConnector\ServiceBus\Command\TransferObjectCommand;
use SystemConnector\ServiceBus\CommandHandler\CommandHandlerInterface;
use SystemConnector\ServiceBus\CommandType;
use SystemConnector\TransferObject\Product\Stock\Stock;

class RemoveStockCommandHandler implements CommandHandlerInterface
{
    /**
     * @var IdentityServiceInterface
     */
    private $identityService;

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(
        IdentityServiceInterface $identityService,
        LoggerInterface $logger
    ) {
        $this->identityService = $identityService;
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function supports(CommandInterface $command)
    {
        return $command instanceof TransferObjectCommand &&
            $command->getAdapterName() === ShopwareAdapter::NAME &&
            $command->getObjectType() === Stock::TYPE &&
            $command->getCommandType() === CommandType::REMOVE;
    }

    /**
     * {@inheritdoc}
     *
     * @param TransferObjectCommand $command
     */
    public function handle(CommandInterface $command)
    {
        $identifier = $command->getPayload();

        $identity = $this->identityService->findOneBy([
            'objectIdentifier' => (string) $identifier,
            'objectType' => Stock::TYPE,
            'adapterName' => ShopwareAdapter::NAME,
        ]);

        if (null === $identity) {
            $this->logger->notice('no matching identity found', ['command' => $command]);

            return false;
        }

        $identities = $this->identityService->findBy([
            'objectIdentifier' => $identifier,
        ]);

        array_walk($identities, function (Identity $identity) {
            $this->identityService->remove($identity);
        });

        return true;
    }
}


<?php

namespace ShopwareAdapter\DataProvider\CustomerGroup;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Shopware\Models\Customer\Group;

class CustomerGroupDataProvider implements CustomerGroupDataProviderInterface
{
    /**
     * @var EntityRepository
     */
    private $repository;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->repository = $entityManager->getRepository(Group::class);
    }

    /**
     * {@inheritdoc}
     */
    public function getCustomerGroupKeyByShopwareIdentifier($identifier)
    {
        /**
         * @var null|Group $group
         */
        $group = $this->repository->find($identifier);

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

        return $group->getKey();
    }
}


<?php

namespace SystemConnector\ServiceBus\CommandFactory;

use Assert\Assertion;
use SystemConnector\ServiceBus\Command\TransferObjectCommand;
use SystemConnector\ServiceBus\CommandFactory\Exception\MissingCommandException;
use SystemConnector\ServiceBus\CommandType;
use SystemConnector\TransferObject\TransferObjectInterface;

class CommandFactory implements CommandFactoryInterface
{
    /**
     * {@inheritdoc}
     */
    public function create($adapterName, $objectType, $commandType, $priority, $payload)
    {
        Assertion::string($adapterName);
        Assertion::string($objectType);
        Assertion::inArray($commandType, CommandType::getAllTypes());
        Assertion::integer($priority);

        if ($commandType === CommandType::HANDLE) {
            Assertion::isInstanceOf($payload, TransferObjectInterface::class);
        }

        if ($commandType === CommandType::REMOVE) {
            Assertion::uuid($payload);
        }

        $command = null;

        switch ($commandType) {
            case CommandType::HANDLE:
                $command = new TransferObjectCommand($adapterName, $objectType, $commandType, $priority, $payload);

                break;
            case CommandType::REMOVE:
                $command = new TransferObjectCommand($adapterName, $objectType, $commandType, $priority, $payload);

                break;
        }

        if (null === $command) {
            throw MissingCommandException::fromObjectData($objectType, $commandType);
        }

        return $command;
    }
}


<?php

namespace Uploadcare;

class Uploader
{
    /**
     * Base upload host
     *
     * @var string
     */
    private $host = 'upload.uploadcare.com';

    /**
     * Api instance
     *
     * @var Api
     */
    private $api = null;

    /**
     * Constructor
     * @param Api $api
     */
    public function __construct(Api $api)
    {
        $this->api = $api;
    }

    /**
     * Check file status.
     * Return array of json data
     *
     * @param string $token
     * @return object
     * @throws \Exception
     */
    public function status($token)
    {
        $data = array(
            'token' => $token,
        );
        $ch = $this->__initRequest('from_url/status', $data);
        $this->__setHeaders($ch);
        $data = $this->__runRequest($ch);
        return $data;
    }

    public function __call($method, $arguments)
    {
        if ($method == 'fromUrl') {
            if (count($arguments) == 1) {
                return call_user_func_array(array($this,'fromUrlNew'), $arguments);
            }
            if (count($arguments) == 2) {
                if (is_array($arguments[1])) {
                    return call_user_func_array(array($this,'fromUrlNew'), $arguments);
                } else {
                    return call_user_func_array(array($this,'fromUrlOld'), $arguments);
                }
            } elseif (count($arguments) >= 3) {
                return call_user_func_array(array($this,'fromUrlOld'), $arguments);
            }
        }
    }

    /**
     * Upload file from a URL and get File instance
     *
     * @deprecated 2.0.0 please use fromUrl($url, $options) instead
     * @param string $url A URL of file to be uploaded.
     * @param boolean $check_status Wait till upload is complete
     * @param int $timeout Wait $timeout seconds between status checks
     * @param int $max_attempts Check status no more than $max_attempts times
     * @throws \Exception
     * @return File|string
     */
    private function fromUrlOld($url, $check_status = true, $timeout = 1, $max_attempts = 5)
    {
        Helper::deprecate('2.0.0', '3.0.0', 'This version of method `fromUrl($url, $check_status, $timeout, $max_attempts)` is deprecated please use `fromUrl($url, $options)` instead');
        return $this->fromUrlNew($url, array(
            'check_status' => $check_status,
            'timeout' => $timeout,
            'max_attempts' => $max_attempts,
        ));
    }

    /**
     * Upload file from a URL and get File instance
     *
     * @param string $url A URL of file to be uploaded.
     * @param array $options Optional dictionary with additional params. Available keys are following:
     *   'store' - can be true, false or 'auto'. This flag indicates should file be stored automatically after upload.
     *   'filename' - should be a string, Sets explicitly file name of uploaded file.
     *   'check_status' - Wait till upload is complete
     *   'timeout' - Wait number of seconds between status checks
     *   'max_attempts' - Check status no more than passed number of times
     * @throws \Exception
     * @return File|string
     */
    private function fromUrlNew($url, $options = array())
    {
        $default_options = array(
            'store' => 'auto',
            'filename' => null,
            'check_status' => true,
            'timeout' => 1,
            'max_attempts' => 5,
        );
        $params = array_merge($default_options, $options);
        $check_status = $params['check_status'];
        $timeout = $params['timeout'];
        $max_attempts = $params['max_attempts'];

        $requestData = array(
            '_' => time(),
            'source_url' => $url,
            'pub_key' => $this->api->getPublicKey(),
            'store' => $params['store'],
        );
        if ($params['filename']) {
            $requestData['filename'] = $params['filename'];
        }

        $ch = $this->__initRequest('from_url', $requestData);
        $this->__setHeaders($ch);

        $data = $this->__runRequest($ch);
        $token = $data->token;

        if ($check_status) {
            $success = false;
            $attempts = 0;
            while (!$success) {
                $data = $this->status($token);
                if ($data->status == 'success') {
                    $success = true;
                }
                if ($data->status == 'error') {
                    throw new \Exception('Upload is not successful: ' . $data->error);
                }
                if ($attempts == $max_attempts && $data->status != 'success') {
                    throw new \Exception('Max attempts reached, upload is not successful');
                }
                sleep($timeout);
                $attempts++;
            }
        } else {
            return $token;
        }
        $uuid = $data->uuid;

        return new File($uuid, $this->api);
    }

    /**
     * Upload file from local path.
     *
     * @param string $path
     * @param string|bool $mime_type
     * @return File
     * @throws \Exception
     */
    public function fromPath($path, $mime_type = false)
    {
        if (function_exists('curl_file_create')) {
            if ($mime_type) {
                $f = curl_file_create($path, $mime_type);
            } else {
                $f = curl_file_create($path);
            }
        } else {
            if ($mime_type) {
                $f = '@' . $path . ';type=' . $mime_type;
            } else {
                $f = '@' . $path;
            }
        }

        $data = array(
            'UPLOADCARE_PUB_KEY' => $this->api->getPublicKey(),
            'file' => $f,
        );
        $ch = $this->__initRequest('base');
        $this->__setRequestType($ch);
        $this->__setData($ch, $data);
        $this->__setHeaders($ch);

        $data = $this->__runRequest($ch);
        $uuid = $data->file;
        return new File($uuid, $this->api);
    }

    /**
     * Upload file from file pointer
     *
     * @param resource $fp
     * @return File
     * @throws \Exception
     */
    public function fromResource($fp)
    {
        $tmpfile = tempnam(sys_get_temp_dir(), 'ucr');
        $temp = fopen($tmpfile, 'w');
        while (!feof($fp)) {
            fwrite($temp, fread($fp, 8192));
        }
        fclose($temp);
        fclose($fp);

        return $this->fromPath($tmpfile);
    }

    /**
     * Upload file from string using mime-type.
     *
     * @param string $content
     * @param string $mime_type
     * @return File
     * @throws \Exception
     */
    public function fromContent($content, $mime_type)
    {
        $tmpfile = tempnam(sys_get_temp_dir(), 'ucr');
        $temp = fopen($tmpfile, 'w');
        fwrite($temp, $content);
        fclose($temp);

        return $this->fromPath($tmpfile, $mime_type);
    }

    /**
     * Create group from array of File objects
     *
     * @param array $files
     * @return Group
     * @throws \Exception
     */
    public function createGroup($files)
    {
        $data = array(
            'pub_key' => $this->api->getPublicKey(),
        );
        /**
         * @var File $file
         */
        foreach ($files as $i => $file) {
            $data["files[$i]"] = $file->getUrl();
        }

        $ch = $this->__initRequest('group');
        $this->__setRequestType($ch);
        $this->__setData($ch, $data);
        $this->__setHeaders($ch);

        $resp = $this->__runRequest($ch);
        $group = $this->api->getGroup($resp->id);
        return $group;
    }

    /**
     * Init upload request and return curl resource
     *
     * @param $type
     * @param array $data
     * @return resource
     */
    private function __initRequest($type, $data = null)
    {
        $url = sprintf('https://%s/%s/', $this->host, $type);
        if (is_array($data)) {
            $url = sprintf('%s?%s', $url, http_build_query($data));
        }
        $ch = curl_init($url);
        return $ch;
    }

    /**
     * Set request type for curl resource
     *
     * @param resource $ch
     * @return void
     */
    private function __setRequestType($ch)
    {
        curl_setopt($ch, CURLOPT_POST, true);
    }

    /**
     * Set all the headers for request and set return transfer.
     *
     * @param resource $ch. Curl resource.
     * @return void
     */
    private function __setHeaders($ch)
    {
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(
            'User-Agent: ' . $this->api->getUserAgentHeader(),
        ));
    }

    /**
     * Set data to be posted on request
     *
     * @param resource $ch. Curl resource
     * @param array $data
     * @return void
     */
    private function __setData($ch, $data = array())
    {
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    }

    /**
     * Run prepared curl request.
     * Throws Exception of not 200 http code
     *
     * @param resource $ch. Curl resource
     * @throws \Exception
     * @return object
     */
    private function __runRequest($ch)
    {
        $data = curl_exec($ch);
        $ch_info = curl_getinfo($ch);
        if ($data === false) {
            throw new \Exception(curl_error($ch));
        } elseif ($ch_info['http_code'] != 200) {
            throw new \Exception('Unexpected HTTP status code ' . $ch_info['http_code'] . '.' . curl_error($ch));
        }
        curl_close($ch);
        return json_decode($data);
    }
}


<?php

namespace Uploadcare;

class Widget
{
    /**
     * Uploadcare widget version
     */
    const VERSION = '3.x';

    /**
     * Api instance
     *
     * @var Api
     */
    private $api = null;

    /**
     * Constructor
     *
     * @param Api $api
     */
    public function __construct(Api $api)
    {
        $this->api = $api;
    }

    /**
     * Returns <script> sections to include Uploadcare widget
     *
     * @param string $version Uploadcare version
     * @param bool $async
     * @return string
     */
    public function getScriptTag($version = null, $async = false, $full = true)
    {
        $async_attr = $async ? 'async="true"' : '';
        $result = <<<EOT
<script>UPLOADCARE_PUBLIC_KEY = "{$this->api->getPublicKey()}";</script>
<script {$async_attr} src="{$this->getScriptSrc($version, $full)}" charset="UTF-8"></script>
EOT;
        return $result;
    }

    /**
     * Return URL for javascript widget.
     * If no version is provided method will use default(current) version
     *
     * @param string $version Version of Uploadcare.com widget
     * @return string
     */
    public function getScriptSrc($version = null, $full = true)
    {
        if (!$version) {
            $version = self::VERSION;
        }
        if ($full) {
            $tail = "uploadcare.full.min.js";
        } else {
            $tail = "uploadcare.min.js";
        }

        return sprintf($this->api->getCdnUri() . '/libs/widget/%s/'. $tail, $version);
    }

    /**
     * Return data-integration attribute.
     *
     * @return string
     */
    public function getIntegrationData()
    {
        $integrationData = '';

        $framework = $this->api->getFramework();
        if ($framework) {
            $integrationData .= $framework;
        }

        $extension = $this->api->getExtension();
        if ($extension) {
            $integrationData .= '; '.$extension;
        }

        return $integrationData;
    }

    /**
     * Gets input tag to use with widget
     *
     * @param string $name Input name
     * @param array $attributes Custom attributes to include
     * @return string
     */
    public function getInputTag($name, $attributes = array())
    {
        $to_compile = array();
        foreach ($attributes as $key => $value) {
            $to_compile[] = sprintf('%s="%s"', $key, $value);
        }
        return sprintf('<input type="hidden" role="uploadcare-uploader" name="%s" data-upload-url-base="" data-integration="%s" %s />', $name, $this->getIntegrationData(), join(' ', $to_compile));
    }
}


<?php

namespace Uploadcare;

use Uploadcare\Exceptions\ThrottledRequestException;

class Api
{
    /**
     * Uploadcare library version
     */
    const UPLOADCARE_LIBRARY_VERSION = '2.2.1';

    /**
     * Uploadcare public key
     *
     * @var string
     */
    private $public_key = null;

    /**
     * Uploadcare secret key
     *
     * @var string
     */
    private $secret_key = null;

    /**
     * API host for requests
     *
     * @var string
     */
    private $api_host = 'api.uploadcare.com';

    /**
     * Current request method
     *
     * @var string
     */
    private $current_method = null;

    /**
     * Uploadcare CDN host
     *
     * @var string
     */
    public $cdn_host = 'ucarecdn.com';

    /**
     * Uploadcare CDN protocol
     *
     * @var string
     */
    public $cdn_protocol = 'https';

    /**
     * Retry throttled requests this number of times
     *
     * @var int
     */
    private $retry_throttled = 1;

    /**
     * Library name for HTTP headers
     *
     * @var string
     */
    protected $libraryName = 'PHPUploadcare';

    /**
     * User agent name for HTTP headers
     *
     * @var string
     */
    protected $userAgentName = null;

    /**
     * Maximum files number can be processed in file batch operations
     *
     * @var int
     */
    private $batchFilesChunkSize = 100;

    /**
     * Widget instance.
     *
     * @var Widget
     */
    public $widget = null;

    /**
     * Uploader instance
     *
     * @var Uploader
     */
    public $uploader = null;

    /**
     * Uploadcare rest API version
     *
     * @var string
     */
    public $api_version = '0.5';

    /**
     * Default values of filters
     *
     * @var array
     */
    private $defaultFilters = array(
        'file' => array(
            'stored' => null,
            'removed' => false,
        ),
    );

    /**
     * Framework name to report with version.
     *
     * @var string
     */
    protected $framework = '';

    /**
     * Extension name to report with version.
     *
     * @var string
     */
    protected $extension = '';

    /**
     * Constructor
     *
     * @param string $public_key A public key given by Uploadcare.com
     * @param string $secret_key A private (secret) key given by Uploadcare.com
     * @param string $userAgentName Custom User agent name to report
     * @param string $cdn_host CDN Host
     * @param string $cdn_protocol CDN Protocol
     * @param integer $retry_throttled Retry throttled requests this number of times
     */
    public function __construct($public_key, $secret_key, $userAgentName = null, $cdn_host = null, $cdn_protocol = null, $retry_throttled = null)
    {
        $this->public_key = $public_key;
        $this->secret_key = $secret_key;
        $this->widget = new Widget($this);
        $this->uploader = new Uploader($this);
        if ($cdn_host !== null) {
            $this->cdn_host = $cdn_host;
        }
        if ($cdn_protocol !== null) {
            $this->cdn_protocol = $cdn_protocol;
        }
        if ($retry_throttled !== null) {
            $this->retry_throttled = $retry_throttled;
        }
        if ($userAgentName !== null) {
            $this->userAgentName = $userAgentName;
        }
    }

    /**
     * Return public key
     *
     * @return string
     */
    public function getPublicKey()
    {
        return $this->public_key;
    }

    /**
     * Return lib version
     *
     * @return string
     */
    public function getVersion()
    {
        return self::UPLOADCARE_LIBRARY_VERSION;
    }

    /**
     * Return CDN URI
     *
     * @return string
     */
    public function getCdnUri()
    {
        return $this->cdn_protocol . '://' . $this->cdn_host;
    }

    /**
     * Set framework name with version.
     *
     * @param string $name Framework name (ex: Wordpress, Laravel).
     * @param string $version Framework version (ex: 1.0.1).
     * @return string
     */
    public function setFramework($name, $version)
    {
        return $this->framework = $name.'/'.$version;
    }

    /**
     * Set extension name with version.
     *
     * @param string $name Extension name (ex: PHPUploadcare-Wordpress).
     * @param string $version Extension version (ex: 1.0.1).
     * @return string
     */
    public function setExtension($name, $version)
    {
        return $this->extension = $name.'/'.$version;
    }

    /**
     * Set own User-Agent name for report in HTTP header.
     *
     * @param string $name
     * @return string
     */
    public function setUserAgentName($name)
    {
        return $this->userAgentName = $name;
    }

    /**
     * Get framework name with version.
     *
     * @return string
     */
    public function getFramework()
    {
        return $this->framework;
    }

    /**
     * Get extension name with version.
     *
     * @return string
     */
    public function getExtension()
    {
        return $this->extension;
    }

    /**
     * Get library name.
     *
     * @return string
     */
    public function getLibraryName()
    {
        return $this->libraryName;
    }

    /**
     * Get library name.
     *
     * @return string
     */
    public function getUserAgentName()
    {
        return $this->userAgentName;
    }

    /**
     * Convert datetime from string or \DateTime object to ATOM string
     *
     * @param string|\DateTime $datetime
     * @throws \Exception
     * @return null|string
     */
    public static function dateTimeString($datetime)
    {
        if ($datetime === null) {
            return null;
        }

        if (is_object($datetime) && !($datetime instanceof \DateTime)) {
            throw new \Exception('Only \DateTime objects allowed');
        }

        if (is_string($datetime)) {
            $datetime = new \DateTime($datetime);
        }

        return $datetime->format("Y-m-d\TH:i:s.uP");
    }

    /**
     * Convert boolean to string
     *
     * @param $bool
     * @return string
     */
    public static function booleanString($bool)
    {
        return $bool ? 'true' : 'false';
    }

    /**
     * Get portion of groups from server respecting filters
     *
     * @param array $options
     * @param bool $reverse
     * @return array
     * @throws \Exception
     */
    public function getGroupsChunk($options = array(), $reverse = false)
    {
        $data = $this->__preparedRequest('group_list', 'GET', $options);
        $groups_raw = (array)$data->results;
        $resultArr = array();
        foreach ($groups_raw as $group_raw) {
            $resultArr[] = new Group($group_raw->id, $this);
        }
        return $this->__preparePagedParams($data, $reverse, $resultArr);
    }

    /**
     * Get portion of files from server respecting filters
     *
     * @param array $options
     * @param bool $reverse
     * @return array
     * @throws \Exception
     */
    public function getFilesChunk($options = array(), $reverse = false)
    {
        $data = $this->__preparedRequest('file_list', 'GET', $options);
        $files_raw = (array)$data->results;
        $resultArr = array();
        foreach ($files_raw as $file_raw) {
            $resultArr[] = new File($file_raw->uuid, $this, $file_raw);
        }
        return $this->__preparePagedParams($data, $reverse, $resultArr);
    }

    /**
     * Return count of files respecting filters
     *
     * @param array $options
     * @return mixed
     * @throws \Exception
     */
    public function getFilesCount($options = array())
    {
        $options['limit'] = 1;

        $data = $this->__preparedRequest('file_list', 'GET', $options);

        return $data->total;
    }

    /**
     * Return count of groups respecting filters
     *
     * @param array $options
     * @return mixed
     * @throws \Exception
     */
    public function getGroupsCount($options = array())
    {
        $options['limit'] = 1;

        $data = $this->__preparedRequest('group_list', 'GET', $options);

        return $data->total;
    }

    /**
     * Return an iterator of File objects to work with.
     *
     * This class provides iteration over all uploaded files. You can specify:
     *   - $options['from'] - a DateTime object or string from which objects will be iterated;
     *   - $options['to'] - a DateTime object or string to which objects will be iterated;
     *   - $options['limit'] - a total number of objects to be iterated;
     *     If not specified, all available objects are iterated;
     *   - $options['request_limit'] - a number of objects to be downloaded per request.
     *   - $options['stored'] - True to include only stored files,
     *     False to exclude, Null is default, will not exclude anything;
     *   - $options['removed'] - True to include only removed files,
     *     False to exclude, Null will not exclude anything.
     *     The default is False.
     *   - $options['reversed'] - If True then result list will be reversed
     *
     * @param array $options
     * @return FileIterator
     * @throws \Exception
     */
    public function getFileList($options = array())
    {
        $options = array_replace(array(
            'from' => null,
            'to' => null,
            'limit' => null,
            'request_limit' => null,
            'stored' => $this->defaultFilters['file']['stored'],
            'removed' => $this->defaultFilters['file']['removed'],
            'reversed' => false,
        ), $options);

        if (!empty($options['from']) && !empty($options['to'])) {
            throw new \Exception('Only one of "from" and "to" arguments is allowed');
        }

        $options['from'] = self::dateTimeString($options['from']);
        $options['to'] = self::dateTimeString($options['to']);

        foreach ($this->defaultFilters['file'] as $k => $v) {
            if (!is_null($options[$k])) {
                $options[$k] = self::booleanString($options[$k]);
            }
        }

        return new FileIterator($this, $options);
    }

    /**
     * Return an iterator of Group objects to work with.
     *
     * This class provides iteration over all uploaded file groups. You can specify:
     *   - $options['from'] - a DateTime object or string from which objects will be iterated;
     *   - $options['to'] - a DateTime object or string to which objects will be iterated;
     *   - $options['limit'] - a total number of objects to be iterated;
     *     If not specified, all available objects are iterated;
     *   - $options['request_limit'] - a number of objects to be downloaded per request.
     *   - $options['reversed'] - If True then result list will be reversed
     *
     * @param array $options
     * @return GroupIterator
     * @throws \Exception
     */
    public function getGroupList($options = array())
    {
        $options = array_replace(array(
            'from' => null,
            'to' => null,
            'limit' => null,
            'request_limit' => null,
            'reversed' => false,
        ), $options);

        if (!empty($options['from']) && !empty($options['to'])) {
            throw new \Exception('Only one of "from" and "to" arguments is allowed');
        }

        $options['from'] = self::dateTimeString($options['from']);
        $options['to'] = self::dateTimeString($options['to']);

        return new GroupIterator($this, $options);
    }

    /**
     * Get group.
     *
     * @param string $uuid_or_url Uploadcare group UUID or CDN URL
     * @return Group
     * @throws \Exception
     */
    public function getGroup($uuid_or_url)
    {
        return new Group($uuid_or_url, $this);
    }

    /**
     * Copy file
     *
     * @deprecated 2.0.0 Use createLocalCopy() or createRemoteCopy() instead.
     * @param string $source CDN URL or file's uuid you need to copy.
     * @param string $target Name of custom storage connected to your project. Uploadcare storage is used if target is absent.
     * @return File|string
     * @throws \Exception
     */
    public function copyFile($source, $target = null)
    {
        Helper::deprecate('2.0.0', '3.0.0', 'Use createLocalCopy() or createRemoteCopy() instead');
        if (!$target) {
            return $this->createLocalCopy($source);
        } else {
            return $this->createRemoteCopy($source, $target);
        }
    }

    /**
     * Copy file to the Uploadcare storage
     *
     * @param string $source CDN URL or file's uuid you need to copy.
     * @param boolean $store MUST be either true or false. true to store files while copying. If stored, files won’t be automatically deleted within 24 hours after copying. false * to not store files, default.
     * @return File|string
     * @throws \Exception
     */
    public function createLocalCopy($source, $store = true)
    {
        $data = $this->__preparedRequest('file_copy', 'POST', array(), array('source' => $source, 'store' => $store));
        if (array_key_exists('result', (array)$data) == true) {
            if ($data->type == 'file') {
                return new File((string)$data->result->uuid, $this);
            } else {
                return (string)$data->result;
            }
        } else {
            return (string)$data->detail;
        }
    }

    /**
     * Copy file to the external storage
     *
     * @param string $source CDN URL or file's uuid you need to copy.
     * @param string $target Name of custom storage connected to your project. Uploadcare storage is used if target is absent.
     * @param boolean $make_public (Optional) MUST be either true or false. true to make copied files available via public links. false to reverse the behavior.
     * @param string $pattern (Optional) Applies to custom storage usage scenario only. The parameter is used to specify file names Uploadcare passes to custom storages. In case parameter is omitted, custom storage pattern is used.
     *
     * Allowed values:
     *
     * ${default} = ${uuid}/${auto_filename}
     * ${auto_filename} = ${filename} ${effects} ${ext}
     * ${effects} = CDN operations put into a URL
     * ${filename} = original filename, no extension
     * ${uuid} = file UUID
     * ${ext} = file extension, leading dot, e.g. .jpg
     *
     * @return string
     * @throws \Exception
     */
    public function createRemoteCopy($source, $target, $make_public = true, $pattern = null)
    {
        if (!$target) {
            throw new \Exception('$target parameter should not be empty.');
        }
        $paramArr = array('source' => $source, 'target' => $target, 'make_public' => $make_public);
        if ($pattern) {
            $paramArr['pattern'] = $pattern;
        }
        $data = $this->__preparedRequest('file_copy', 'POST', array(), $paramArr);
        if (array_key_exists('result', (array)$data) == true) {
            return (string)$data->result;
        } else {
            return (string)$data->detail;
        }
    }

    /**
     * Store multiple files
     *
     * @param array $filesUuidArr uploaded file's uuid array you need to store.
     * @return array with stored files and problems if any
     * @throws \Exception
     */
    public function storeMultipleFiles($filesUuidArr)
    {
        return $this->__batchProcessFiles($filesUuidArr, 'PUT');
    }

    /**
     * Delete multiple files
     *
     * @param array $filesUuidArr uploaded or stored file's uuid array you need to delete.
     * @return array with deleted files and problems if any
     * @throws \Exception
     */
    public function deleteMultipleFiles($filesUuidArr)
    {
        return $this->__batchProcessFiles($filesUuidArr, 'DELETE');
    }

    /**
     * Process multiple files with chunk support
     *
     * @param array $filesUuidArr uploaded file's uuid array you need to process.
     * @param string $request_type request type, could be PUT or DELETE .
     * @return array with processed files and problems if any
     * @throws \Exception
     */
    public function __batchProcessFiles($filesUuidArr, $request_type)
    {
        $filesChunkedArr = array_chunk($filesUuidArr, $this->batchFilesChunkSize);
        $filesArr = array();
        $problemsArr = array();
        $lastStatus = '';
        foreach ($filesChunkedArr as $chunk) {
            $res = $this->__batchProcessFilesChunk($chunk, $request_type);
            $lastStatus = $res['status'];
            if ($lastStatus == "ok") {
                $problemsObj = $res['problems'];
                if (count(get_object_vars($problemsObj)) > 0) {
                    $problemsArr [] = $problemsObj;
                }
                $filesArr = array_merge($filesArr, $res['files']);
            } else {
                throw new \Exception('Error process multiple files', $res);
            }
        }
        return array(
            'status' => $lastStatus,
            'files' => $filesArr,
            'problems' => $problemsArr,
        );
    }

    /**
     * Run request to process multiple files
     *
     * @param array $filesUuidArr uploaded file's uuid array you need to process.
     * @param string $request_type request type, could be PUT or DELETE .
     * @return array with processed files and problems if any
     * @throws \Exception
     */
    public function __batchProcessFilesChunk($filesUuidArr, $request_type)
    {
        if (count($filesUuidArr) > $this->batchFilesChunkSize) {
            throw new \Exception('Files number should not exceed '.$this->batchFilesChunkSize.' items per request.');
        }
        $data = $this->__preparedRequest('files_storage', $request_type, array(), $filesUuidArr);
        $files_raw = (array)$data->result;
        $result = array();
        foreach ($files_raw as $file_raw) {
            $result[] = new File($file_raw->uuid, $this, $file_raw);
        }
        return array(
            'status' => (string)$data->status,
            'files' => $result,
            'problems' => $data->problems,
        );
    }

    /**
     * Run raw request to REST.
     *
     * @param string $method Request method: GET, POST, HEAD, OPTIONS, PUT, etc
     * @param string $path Path to request
     * @param array $data Array of data to send.
     * @param array $headers Additional headers.
     * @throws \Exception
     * @return object
     */
    public function request($method, $path, $data = array(), $headers = array())
    {
        $ch = curl_init(sprintf('https://%s%s', $this->api_host, $path));
        $this->__setRequestType($ch, $method);
        $this->__setHeaders($ch, $headers, $data);

        $response = curl_exec($ch);
        if ($response === false) {
            throw new \Exception(curl_error($ch));
        }
        $ch_info = curl_getinfo($ch);

        $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $header = substr($response, 0, $header_size);
        $body = substr($response, $header_size);

        $error = false;

        if ($method == 'DELETE') {
            if ($ch_info['http_code'] != 302 && $ch_info['http_code'] != 200) {
                $error = true;
            }
        } else {
            if (!(($ch_info['http_code'] >= 200) && ($ch_info['http_code'] < 300))) {
                $error = true;
            }
        }

        if ($ch_info['http_code'] == 429) {
            $exception = new ThrottledRequestException();
            $response_headers = Helper::parseHttpHeaders($header);
            $exception->setResponseHeaders($response_headers);
            throw $exception;
        }

        if ($error) {
            $errorInfo = array_filter(array(curl_error($ch), $body));

            throw new \Exception('Request returned unexpected http code '. $ch_info['http_code'] . '. ' . join(', ', $errorInfo));
        }

        curl_close($ch);
        if (!defined('PHPUNIT_UPLOADCARE_TESTSUITE') && ($this->public_key == 'demopublic_key' || $this->secret_key == 'demoprivatekey')) {
            trigger_error('You are using the demo account. Please get an Uploadcare account at https://uploadcare.com/accounts/create/', E_USER_WARNING);
        }

        return json_decode($body);
    }

    /**
     * Make request to API.
     * Throws Exception if not http code 200 was returned.
     * If http code 200 it will parse returned data form request as JSON.
     *
     * @param string $type Construct type. URL will be generated using this params. Options: store
     * @param string $request_type Request type. Options: get, post, put, delete.
     * @param array $params Additional parameters for requests as array.
     * @param array $data Data will be posted like json.
     * @param null $retry_throttled
     * @throws \Exception
     * @throws \Uploadcare\Exceptions\ThrottledRequestException
     * @return object|null
     */
    public function __preparedRequest($type, $request_type = 'GET', $params = array(), $data = array(), $retry_throttled = null)
    {
        $retry_throttled = $retry_throttled ?: $this->retry_throttled;
        $path = $this->__getPath($type, $params);

        while (true) {
            try {
                return $this->request($request_type, $path, $data);
            } catch (ThrottledRequestException $exception) {
                if ($retry_throttled > 0) {
                    sleep($exception->getTimeout());
                    $retry_throttled--;
                    continue;
                } else {
                    throw $exception;
                }
            }
        }

        return null;
    }

    /**
     * Prepares paged params array from chunk request result.
     *
     * @param object $data
     * @param boolean $reverse
     * @param array $resultArr
     * @return array
     */
    private function __preparePagedParams($data, $reverse, $resultArr)
    {
        $nextParamsArr = parse_url($data->next);
        $prevParamsArr = parse_url($data->previous);
        $nextParamsArr = array_replace(array('query' => null), $nextParamsArr);
        $prevParamsArr = array_replace(array('query' => null), $prevParamsArr);

        parse_str(parse_url(!$reverse ? $data->next : $data->previous, PHP_URL_QUERY), $params);

        if ($reverse) {
            $resultArr = array_reverse($resultArr);
        }

        return array(
            'nextParams' => $reverse ? $prevParamsArr : $nextParamsArr,
            'prevParams' => !$reverse ? $prevParamsArr : $nextParamsArr,
            'params' => $params,
            'data' => $resultArr,
        );
    }

    /**
     * Convert query array to encoded query string.
     *
     * @param array $queryAr
     * @param string $prefixIfNotEmpty
     * @return string
     */
    private function __getQueryString($queryAr = array(), $prefixIfNotEmpty = '')
    {
        $queryAr = array_filter($queryAr);
        array_walk($queryAr, function (&$val, $key) {
            $val = urlencode($key) . '=' . urlencode($val);
        });

        return $queryAr ? $prefixIfNotEmpty . join('&', $queryAr) : '';
    }

    /**
     * Return path to send request to.
     * Throws Exception if wrong type is provided or parameters missing.
     *
     * @param string $type Construct type.
     * @param array $params Additional parameters for requests as array.
     * @throws \Exception
     * @return string
     */
    private function __getPath($type, $params = array())
    {
        switch ($type) {
            case 'root':
                return '/';
            case 'account':
                return '/account/';
            case 'file_list':
                return '/files/' . $this->__getQueryString($params, '?');
            case 'file_storage':
                if (array_key_exists('uuid', $params) == false) {
                    throw new \Exception('Please provide "uuid" param for request');
                }
                return sprintf('/files/%s/storage/', $params['uuid']);
            case 'file_copy':
                return '/files/';
            case 'files_storage':
                return '/files/storage/';
            case 'file':
                if (array_key_exists('uuid', $params) == false) {
                    throw new \Exception('Please provide "uuid" param for request');
                }
                return sprintf('/files/%s/', $params['uuid']);
            case 'group_list':
                return '/groups/' . $this->__getQueryString($params, '?');
            case 'group':
                if (array_key_exists('uuid', $params) == false) {
                    throw new \Exception('Please provide "uuid" param for request');
                }
                return sprintf('/groups/%s/', $params['uuid']);
            case 'group_storage':
                if (array_key_exists('uuid', $params) == false) {
                    throw new \Exception('Please provide "uuid" param for request');
                }
                return sprintf('/groups/%s/storage/', $params['uuid']);
            default:
                throw new \Exception('No api url type is provided for request "' . $type . '". Use store, or appropriate constants.');
        }
    }

    /**
     * Set request type.
     * If request type is wrong an Exception will be thrown.
     *
     * @param resource $ch. Curl resource.
     * @param string $type Request type. Options: get, post, put, delete.
     * @throws \Exception
     * @return void
     */
    private function __setRequestType($ch, $type = 'GET')
    {
        $this->current_method = strtoupper($type);

        switch ($type) {
            case 'GET':
                break;
            case 'POST':
                curl_setopt($ch, CURLOPT_POST, true);
                break;
            case 'PUT':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
                break;
            case 'DELETE':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
                break;
            case 'HEAD':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'HEAD');
                curl_setopt($ch, CURLOPT_NOBODY, true);
                break;
            case 'OPTIONS':
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'OPTIONS');
                break;
            default:
                throw new \Exception('No request type is provided for request. Use post, put, delete, get or appropriate constants.');
        }
    }

    /**
     * Returns full user agent string.
     * @deprecated 2.2.1 Use getUserAgentHeader() instead.
     * @return string
     */
    public function getUserAgent() {
        Helper::deprecate('2.0.0', '3.0.0', 'Use getUserAgentHeader() instead');
        return $this->getUserAgentHeader();
    }

    /**
     * Returns full user agent string.
     *
     * @return string
     */
    public function getUserAgentHeader()
    {
        // If has own User-Agent
        $userAgentName = $this->getUserAgentName();
        if ($userAgentName) {
            return $userAgentName;
        }

        $userAgent = sprintf('%s/%s/%s (PHP/%s.%s.%s', $this->getLibraryName(), $this->getVersion(), $this->getPublicKey(), PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION);

        $framework = $this->getFramework();
        if ($framework) {
            $userAgent .= '; '.$framework;
        }

        $extension = $this->getExtension();
        if ($extension) {
            $userAgent .= '; '.$extension;
        }

        $userAgent .= ')';

        return $userAgent;
    }

    /**
     * Set all the headers for request and set return transfer.
     *
     * @param resource $ch. Curl resource.
     * @param array $add_headers additional headers.
     * @param array $data Data array.
     * @throws \Exception
     * @return void
     */
    private function __setHeaders($ch, $add_headers = array(), $data = array())
    {
        $content_length = 0;
        $rawContent = '';
        if (count($data)) {
            $rawContent = utf8_encode(json_encode($data));
            $content_length = strlen($rawContent);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $rawContent);
        }

        // path
        $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        $url_parts = parse_url($url);

        if ($url_parts === false) {
            throw new \Exception('Incorrect URL ' . $url);
        }

        $path = $url_parts['path'] . (!empty($url_parts['query']) ? '?' . $url_parts['query'] : '');

        // content
        $content_type = 'application/json';
        $content_md5 = md5($rawContent);

        // date
        $date = gmdate('D, d M Y H:i:s \G\M\T');

        // sign string
        $sign_string = join("\n", array(
            $this->current_method,
            $content_md5,
            $content_type,
            $date,
            $path,
        ));
        $sign_string_as_bytes = utf8_encode($sign_string);

        $secret_as_bytes = utf8_encode($this->secret_key);

        $sign = hash_hmac('sha1', $sign_string_as_bytes, $secret_as_bytes);

        $headers = array(
            sprintf('Host: %s', $this->api_host),
            sprintf('Authorization: Uploadcare %s:%s', $this->public_key, $sign),
            sprintf('Date: %s', $date),
            sprintf('Content-Type: %s', $content_type),
            sprintf('Content-Length: %d', $content_length),
            sprintf('Accept: application/vnd.uploadcare-v%s+json', $this->api_version),
            sprintf('User-Agent: %s', $this->getUserAgentHeader()),
        ) + $add_headers;

        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_HEADER, 1);
    }

    /**
     * Get object of File class by id
     *
     * @param string $uuid_or_url Uploadcare file UUID or CDN URL
     * @return File
     * @throws \Exception
     */
    public function getFile($uuid_or_url)
    {
        return new File($uuid_or_url, $this);
    }
}


<?php

namespace Uploadcare;

/**
 * @property array $data File info
 */
class File
{
    /**
     * @var string
     */
    private $re_uuid_with_effects = '!/?(?P<uuid>[a-z0-9]{8}-(?:[a-z0-9]{4}-){3}[a-z0-9]{12})(?:/(?:-/(?P<effects>(?:[^/]+/)+)))?(?<filename>[^/]*)!';

    /**
     * Uploadcare file id
     *
     * @var string
     */
    private $uuid = null;

    /**
     * Operations and params for operations.
     *
     * @var array
     */
    private $operations = array();

    /**
     * Uploadcare class instance.
     *
     * @var Api
     */
    private $api = null;

    /**
     * Operations list
     */
    private $operation_list = array(
        'crop',
        'resize',
        'scale_crop',
        'effect',
        'preview',
    );

    /**
     * Cached data
     *
     * @var array
     */
    private $cached_data = null;

    /**
     * Constructs an object for CDN file with specified ID
     *
     * @param string $uuid_or_url Uploadcare file UUID or CDN URL
     * @param Api $api Uploadcare class instance
     * @param boolean|array|object $data prepopulate this->cached_data
     * @throws \Exception
     */
    public function __construct($uuid_or_url, Api $api, $data = false)
    {
        $matches = array();
        if (!preg_match($this->re_uuid_with_effects, $uuid_or_url, $matches)) {
            throw new \Exception(sprintf('UUID or CDN URL cannot be found in "%s"', $uuid_or_url));
        }

        $this->uuid = $matches['uuid'];
        $this->default_effects = $matches['effects'];
        $this->filename = $matches['filename'];
        $this->api = $api;
        if ($data) {
            $this->cached_data = (array)$data;

            if (array_key_exists('default_effects', $this->cached_data)) {
                $this->default_effects = $this->cached_data['default_effects'];
            }
        }
    }

    /**
     * @param $name
     * @return array|null
     */
    public function __get($name)
    {
        if ($name == 'data') {
            if (!$this->cached_data) {
                $this->updateInfo();
            }
            return $this->cached_data;
        }
        return null;
    }

    public function __isset($name)
    {
        return $name == 'data';
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return $this->getUrl();
    }

    /**
     * Get UUID
     *
     * @returns string
     */
    public function getUuid()
    {
        return $this->uuid;
    }

    /**
     * Get UUID
     *
     * @deprecated 1.0.6 Use getUuid() instead.
     * @return string
     */
    public function getFileId()
    {
        Helper::deprecate('1.0.6', '3.0.0', 'Use getUuid() instead');
        return $this->getUuid();
    }

    /**
     * Update File info
     *
     * @return array
     * @throws \Exception
     */
    public function updateInfo()
    {
        $this->cached_data = (array)$this->api->__preparedRequest('file', 'GET', array('uuid' => $this->uuid));
        return $this->cached_data;
    }

    /**
     * Store file.
     *
     * @return object
     * @throws \Exception
     */
    public function store()
    {
        return $this->api->__preparedRequest('file_storage', 'POST', array('uuid' => $this->uuid));
    }

    /**
     * Copy the file.
     *
     * @deprecated 2.0.0 Use createLocalCopy() or createRemoteCopy() instead.
     * @param string $target Name of custom storage.
     * @return File|string
     * @throws \Exception
     */
    public function copy($target = null)
    {
        Helper::deprecate('2.0.0', '3.0.0', 'Use createLocalCopy() or createRemoteCopy() instead');
        return $this->api->copyFile($this->getUrl(), $target);
    }

    /**
     * Copy the file to custom storage.
     *
     * @deprecated 2.0.0 Use createRemoteCopy() instead.
     * @param string $target Name of custom storage.
     * @return string
     * @throws \Exception
     */
    public function copyTo($target)
    {
        Helper::deprecate('2.0.0', '3.0.0', 'Use createRemoteCopy() instead');
        return (string)$this->copy($target);
    }

    /**
     * Copy file to the Uploadcare storage
     *
     * @param boolean $store MUST be either true or false. true to store files while copying. If stored, files won’t be automatically deleted within 24 hours after copying. false * to not store files, default.
     * @return File|string
     * @throws \Exception
     */
    public function createLocalCopy($store = true)
    {
        return $this->api->createLocalCopy($this->getUrl(), $store);
    }

    /**
     * Copy file to the external storage
     *
     * @param string $target Name of custom storage connected to your project.
     * @param boolean $make_public (Optional) MUST be either true or false. true to make copied files available via public links. false to reverse the behavior.
     * @param string $pattern (Optional) The parameter is used to specify file names Uploadcare passes to custom storages. In case parameter is omitted, custom storage pattern is used.
     *
     * Allowed values:
     *
     * ${default} = ${uuid}/${auto_filename}
     * ${auto_filename} = ${filename} ${effects} ${ext}
     * ${effects} = CDN operations put into a URL
     * ${filename} = original filename, no extension
     * ${uuid} = file UUID
     * ${ext} = file extension, leading dot, e.g. .jpg
     *
     * @return string
     * @throws \Exception
     */
    public function createRemoteCopy($target, $make_public = true, $pattern = '${default}')
    {
        return $this->api->createRemoteCopy($this->getUrl(), $target, $make_public, $pattern);
    }

    /**
     * Delete file
     *
     * @return object|null
     * @throws \Exception
     */
    public function delete()
    {
        return $this->api->__preparedRequest('file_storage', 'DELETE', array('uuid' => $this->uuid));
    }

    /**
     * Get URL of original image
     *
     * @param string $postfix
     * @return string
     */
    public function getUrl($postfix = null)
    {
        $url = sprintf('%s%s', $this->api->getCdnUri(), $this->getPath($postfix));
        return $url;
    }

    /**
     * Get local URL path of original image
     *
     * @param string $postfix
     * @return string
     */
    public function getPath($postfix = null)
    {
        $url = sprintf('/%s/', $this->uuid);
        if ($this->default_effects) {
            $url = sprintf('%s-/%s', $url, $this->default_effects);
        }
        if ($this->filename && $postfix === null) {
            $postfix = $this->filename;
        }

        $operations = array();

        foreach ($this->operations as $i => $operation_item) {
            $part = array();
            foreach (array_keys($operation_item) as $operation_type) {
                $operation_params = $operation_item[$operation_type];
                $part[] = $operation_type;

                switch ($operation_type) {
                    case 'crop':
                        $part = $this->__addPartSize($part, $operation_params);
                        $part = $this->__addPartCenter($part, $operation_params);
                        $part = $this->__addPartFillColor($part, $operation_params);
                        break;
                    case 'resize':
                        $part = $this->__addPartSize($part, $operation_params);
                        break;
                    case 'scale_crop':
                        $part = $this->__addPartSize($part, $operation_params);
                        $part = $this->__addPartCenter($part, $operation_params);
                        break;
                    case 'effect':
                        $part = $this->__addPartEffect($part, $operation_params);
                        break;
                    case 'preview':
                        $part = $this->__addPartSize($part, $operation_params);
                        break;
                    case 'custom':
                        $part = array($operation_params);
                        break;
                }
                $part_str = join('/', $part);
                $operations[] = $part_str;
            }
        }

        if (count($operations)) {
            $operations_part = join('/-/', $operations);
            return $url.'-/'.$operations_part.'/'.$postfix;
        } else {
            return $url.$postfix;
        }
    }

    /**
     * Get image tag
     *
     * @param string $postfix File path postfix
     * @param array $attributes additional attributes
     * @return string
     */
    public function getImgTag($postfix = null, $attributes = array())
    {
        $to_compile = array();
        foreach ($attributes as $key => $value) {
            $to_compile[] = sprintf('%s="%s"', $key, $value);
        }
        return sprintf('<img src="%s" %s />', $this->getUrl(), join(' ', $to_compile));
    }

    /**
     * Get object with cropped parameters.
     *
     * @param int $width Crop width
     * @param int $height Crop height
     * @param boolean $center Center crop? true or false (default false).
     * @param string|boolean $fill_color Fill color. If nothing is provided just use false (default false).
     * @return File
     * @throws \Exception
     */
    public function crop($width, $height, $center = false, $fill_color = false)
    {
        if (!$width || !$height) {
            throw new \Exception('Please provide both $width and $height');
        }
        $result = clone $this;
        $result->operations[]['crop'] = array(
            'width' => $width,
            'height' => $height,
            'center' => $center,
            'fill_color' => $fill_color,
        );
        return $result;
    }

    /**
     * Get object with resized parameters.
     * Provide width or height or both.
     * If not width or height are provided exceptions will be thrown!
     *
     * @param int|boolean $width Resized image width. Provide false if you resize proportionally.
     * @param int|boolean $height Resized image height. Provide false if you resize proportionally.
     * @throws \Exception
     * @return File
     */
    public function resize($width = false, $height = false)
    {
        if (!$width && !$height) {
            throw new \Exception('Please, provide at least $width or $height for resize');
        }
        $result = clone $this;
        $result->operations[]['resize'] = array(
            'width' => $width,
            'height' => $height,
        );
        return $result;
    }

    /**
     * Get object with preview parameters.
     * Provide both width and height.
     * If no width and height are provided exceptions will be thrown!
     *
     * @param int $width Preview image width.
     * @param int $height Preview image height.
     * @throws \Exception
     * @return File
     */
    public function preview($width, $height)
    {
        if (!$width || !$height) {
            throw new \Exception('Please, provide both width and height for preview');
        }
        $result = clone $this;
        $result->operations[]['preview'] = array(
            'width' => $width,
            'height' => $height,
        );
        return $result;
    }

    /**
     * Get object with cropped parameters.
     *
     * @param int $width Crop width
     * @param int $height Crop height
     * @param boolean $center Center crop? true or false (default false).
     * @return File
     * @throws \Exception
     */
    public function scaleCrop($width, $height, $center = false)
    {
        if (!$width || !$height) {
            throw new \Exception('Please, provide both $width and $height');
        }
        $result = clone $this;
        $result->operations[]['scale_crop'] = array(
            'width' => $width,
            'height' => $height,
            'center' => $center,
        );
        return $result;
    }

    /**
     * Apply effect
     *
     * @param string $effect Effect name
     * @return File
     */
    public function effect($effect)
    {
        if (!$effect) {
            return $this;
        }
        $result = clone $this;
        $result->operations[]['effect'] = $effect;
        return $result;
    }

    /**
     * Add any custom operation.
     *
     * @param string $operation
     * @return File
     */
    public function op($operation)
    {
        if (!$operation) {
            return $this;
        }
        $result = clone $this;
        $result->operations[]['custom'] = $operation;
        return $result;
    }

    /**
     * Adds part with size for operations
     *
     * @param array $part
     * @param array $params
     * @return array
     */
    private function __addPartSize($part, $params)
    {
        $part[] = sprintf('%sx%s', $params['width'], $params['height']);
        return $part;
    }

    /**
     * Adds part with center for operations
     *
     * @param array $part
     * @param array $params
     * @return array
     */
    private function __addPartCenter($part, $params)
    {
        if ($params['center'] !== false) {
            $part[] = 'center';
        }
        return $part;
    }

    /**
     * Adds part with fill color for operations
     *
     * @param array $part
     * @param array $params
     * @return array
     */
    private function __addPartFillColor($part, $params)
    {
        if ($params['fill_color'] !== false) {
            $part[] = $params['fill_color'];
        }
        return $part;
    }

    /**
     * Adds part with effect for operations
     *
     * @param array $part
     * @param string $effect
     * @return array
     */
    private function __addPartEffect($part, $effect)
    {
        $part[] = $effect;
        return $part;
    }
}