<?php

namespace atoum\atoum\test\adapter;

use atoum\atoum\exceptions;
use atoum\atoum\test\adapter;

class calls implements \countable, \arrayAccess, \iteratorAggregate
{
    protected $calls = [];
    protected $size = 0;
    protected $decorator = null;

    private static $callsNumber = 0;

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

    public function __invoke()
    {
        return $this->calls;
    }

    public function __toString()
    {
        return $this->decorator->decorate($this);
    }

    #[\ReturnTypeWillChange]
    public function count()
    {
        return $this->size;
    }

    #[\ReturnTypeWillChange]
    public function offsetSet($functionName = null, $call = null)
    {
        if ($functionName !== null) {
            $call->setFunction($functionName);
        }

        $this->addCall($call);
    }

    #[\ReturnTypeWillChange]
    public function offsetGet($mixed)
    {
        return $this->getEqualTo(self::buildCall($mixed));
    }

    #[\ReturnTypeWillChange]
    public function offsetUnset($mixed)
    {
        $key = self::getKey(self::buildCall($mixed));

        if (isset($this->calls[$key]) === true) {
            $this->size -= count($this->calls[$key]);

            unset($this->calls[$key]);
        }
    }

    #[\ReturnTypeWillChange]
    public function offsetExists($mixed)
    {
        return (isset($this->calls[self::getKey(self::buildCall($mixed))]) === true);
    }

    #[\ReturnTypeWillChange]
    public function getIterator()
    {
        return new \arrayIterator($this());
    }

    public function reset()
    {
        $this->calls = [];
        $this->size = 0;

        return $this;
    }

    public function setDecorator(?adapter\calls\decorator $decorator = null)
    {
        $this->decorator = $decorator ?: new adapter\calls\decorator();

        return $this;
    }

    public function getDecorator()
    {
        return $this->decorator;
    }

    public function addCall(adapter\call $call)
    {
        return $this->setCall($call);
    }

    public function removeCall(adapter\call $call, $position)
    {
        $function = $call->getFunction();

        if ($function == '') {
            throw new exceptions\logic\invalidArgument('Function is undefined');
        }

        $key = self::getKey($call);

        if (isset($this->calls[$key][$position]) === true) {
            unset($this->calls[$key][$position]);
            $this->size--;
        }

        return $this;
    }

    public function toArray(?adapter\call $call = null)
    {
        if ($call === null) {
            $calls = $this->getTimeline();
        } else {
            $calls = $this->getEqualTo($call)->toArray();
        }

        return $calls;
    }

    public function getEqualTo(adapter\call $call)
    {
        $innerCalls = $this->getCalls($call);

        if ($call->getArguments() !== null || $call->getVerify() !== null) {
            $innerCalls = array_filter($innerCalls, function ($innerCall) use ($call) {
                return $call->isEqualTo($innerCall);
            });
        }

        return self::buildCallsForCall($call, $innerCalls);
    }

    public function getIdenticalTo(adapter\call $call)
    {
        $innerCalls = $this->getCalls($call);

        if ($call->getArguments() !== null) {
            $innerCalls = array_filter($innerCalls, function ($innerCall) use ($call) {
                return $call->isIdenticalTo($innerCall);
            });
        }

        return self::buildCallsForCall($call, $innerCalls);
    }

    public function getPreviousEqualTo(adapter\call $call, $position)
    {
        $calls = new static();

        foreach ($this->getEqualTo($call)->toArray() as $innerPosition => $innerCall) {
            if ($innerPosition < $position) {
                $calls->setCall($innerCall, $innerPosition);
            }
        }

        return $calls;
    }

    public function getPreviousIdenticalTo(adapter\call $call, $position)
    {
        $calls = new static();

        foreach ($this->getIdenticalTo($call)->toArray() as $innerPosition => $innerCall) {
            if ($innerPosition < $position) {
                $calls->setCall($innerCall, $innerPosition);
            }
        }

        return $calls;
    }

    public function getPrevious(adapter\call $call, $position, $identical = false)
    {
        return ($identical === false ? $this->getPreviousEqualTo($call, $position) : $this->getPreviousIdenticalTo($call, $position));
    }

    public function hasPreviousEqualTo(adapter\call $call, $position)
    {
        foreach ($this->getEqualTo($call)->toArray() as $innerPosition => $_) {
            if ($innerPosition < $position) {
                return true;
            }
        }

        return false;
    }

    public function hasPreviousIdenticalTo(adapter\call $call, $position)
    {
        foreach ($this->getIdenticalTo($call)->toArray() as $innerPosition => $_) {
            if ($innerPosition < $position) {
                return true;
            }
        }

        return false;
    }

    public function hasPrevious(adapter\call $call, $position, $identical = false)
    {
        return ($identical === false ? $this->hasPreviousEqualTo($call, $position) : $this->hasPreviousIdenticalTo($call, $position));
    }

    public function getAfterEqualTo(adapter\call $call, $position)
    {
        $calls = new static();

        foreach ($this->getEqualTo($call)->toArray() as $innerPosition => $innerCall) {
            if ($innerPosition > $position) {
                $calls->setCall($innerCall, $innerPosition);
            }
        }

        return $calls;
    }

    public function getAfterIdenticalTo(adapter\call $call, $position)
    {
        $calls = new static();

        foreach ($this->getIdenticalTo($call)->toArray() as $innerPosition => $innerCall) {
            if ($innerPosition > $position) {
                $calls->setCall($innerCall, $innerPosition);
            }
        }

        return $calls;
    }

    public function getAfter(adapter\call $call, $position, $identical = false)
    {
        return ($identical === false ? $this->getAfterEqualTo($call, $position) : $this->getAfterIdenticalTo($call, $position));
    }

    public function hasAfterEqualTo(adapter\call $call, $position)
    {
        foreach ($this->getEqualTo($call)->toArray() as $innerPosition => $_) {
            if ($innerPosition > $position) {
                return true;
            }
        }

        return false;
    }

    public function hasAfterIdenticalTo(adapter\call $call, $position)
    {
        foreach ($this->getIdenticalTo($call)->toArray() as $innerPosition => $innerCall) {
            if ($innerPosition > $position) {
                return true;
            }
        }

        return false;
    }

    public function hasAfter(adapter\call $call, $position, $identical = false)
    {
        return ($identical === false ? $this->hasAfterEqualTo($call, $position) : $this->hasAfterIdenticalTo($call, $position));
    }

    public function get(adapter\call $call, $identical = false)
    {
        return ($identical === false ? $this->getEqualTo($call) : $this->getIdenticalTo($call));
    }

    public function getTimeline()
    {
        $timeline = [];

        foreach ($this as $innerCalls) {
            foreach ($innerCalls as $position => $call) {
                $timeline[$position] = $call;
            }
        }

        ksort($timeline, SORT_NUMERIC);

        return $timeline;
    }

    protected function setCall(adapter\call $call, $position = null)
    {
        $function = $call->getFunction();

        if ($function == '') {
            throw new exceptions\logic\invalidArgument('Function is undefined');
        }

        // A bug in PHP removes this static attribute when destructing all the
        // object. So accessing it create an error. This is a workaround.
        if (!isset(self::$callsNumber)) {
            return $this;
        }

        if ($position === null) {
            $position = ++self::$callsNumber;
        }

        $this->calls[self::getKey($call)][$position] = $call;
        $this->size++;

        return $this;
    }

    protected function getCalls($mixed)
    {
        $key = self::getKey($mixed);

        return (isset($this->calls[$key]) === false ? [] : $this->calls[$key]);
    }

    protected static function getKey(adapter\call $call)
    {
        return strtolower($call->getFunction());
    }

    private static function buildCallsForCall(adapter\call $call, array $array)
    {
        $calls = new static();

        $calls->calls[self::getKey($call)] = $array;
        $calls->size = count($array);

        return $calls;
    }

    private static function buildCall($mixed)
    {
        return ($mixed instanceof adapter\call ? $mixed : new adapter\call($mixed));
    }
}
