<?php

namespace atoum\atoum\asserters;

use atoum\atoum\exceptions;

class phpArray extends variable implements \arrayAccess
{
    private $key = null;
    private $innerAsserter = null;
    private $innerAsserterUsed = false;
    private $innerValue = null;
    private $innerValueIsSet = false;

    public function __get($asserter)
    {
        switch (strtolower($asserter)) {
            case 'keys':
                return $this->getKeysAsserter();

            case 'values':
                return $this->getValuesAsserter();

            case 'size':
                return $this->getSizeAsserter();

            case 'isempty':
                return $this->isEmpty();

            case 'isnotempty':
                return $this->isNotEmpty();

            case 'child':
                $asserter = new phpArray\child($this);

                $this->resetInnerAsserter();

                return $asserter->setWith($this->value);

            default:
                $asserter = parent::__get($asserter);

                if ($asserter instanceof variable === false) {
                    $this->resetInnerAsserter();

                    return $asserter;
                } else {
                    if ($this->innerAsserter === null || $this->innerAsserterUsed === true) {
                        $this->innerValue = $this->value;
                        $this->innerAsserterUsed = false;
                    }

                    $this->innerAsserter = $asserter;

                    return $this;
                }
        }
    }

    public function __call($method, $arguments)
    {
        if ($this->innerAsserterCanUse($method) === false) {
            return parent::__call($method, $arguments);
        } else {
            return $this->callInnerAsserterMethod($method, $arguments);
        }
    }

    public function getKey()
    {
        return $this->key;
    }

    public function getInnerAsserter()
    {
        return $this->innerAsserter;
    }

    public function getInnerValue()
    {
        return $this->innerValue;
    }

    public function reset()
    {
        $this->key = null;

        return parent::reset()->resetInnerAsserter();
    }

    #[\ReturnTypeWillChange]
    public function offsetGet($key)
    {
        if ($this->innerAsserter === null) {
            if ($this->analyzer->isArray($this->hasKey($key)->value[$key]) === true) {
                parent::setWith($this->value[$key]);
            } else {
                $this->fail($this->_('Value %s at key %s is not an array', $this->getTypeOf($this->value[$key]), $key));
            }
        } else {
            if (array_key_exists($key, $this->innerValue) === false) {
                $this->fail($this->_('%s has no key %s', $this->getTypeOf($this->innerValue), $this->getTypeOf($key)));
            } else {
                $this->innerValue = $this->innerValue[$key];
                $this->innerValueIsSet = true;
            }
        }

        return $this;
    }

    #[\ReturnTypeWillChange]
    public function offsetSet($key, $value)
    {
        throw new exceptions\logic('Tested array is read only');
    }

    #[\ReturnTypeWillChange]
    public function offsetUnset($key)
    {
        throw new exceptions\logic('Array is read only');
    }

    #[\ReturnTypeWillChange]
    public function offsetExists($key)
    {
        $value = ($this->innerAsserter === null ? $this->value : $this->innerValue);

        return ($value !== null && array_key_exists($key, $value) === true);
    }

    public function setWith($value, $checkType = true)
    {
        $innerAsserter = $this->innerAsserter;

        if ($innerAsserter !== null) {
            $this->reset();

            return $innerAsserter->setWith($value);
        } else {
            parent::setWith($value);

            if ($this->analyzer->isArray($this->value) === true  || $checkType === false) {
                $this->pass();
            } else {
                $this->fail($this->_('%s is not an array', $this));
            }

            return $this;
        }
    }

    public function setByReferenceWith(& $value)
    {
        if ($this->innerAsserter !== null) {
            return $this->innerAsserter->setByReferenceWith($value);
        } else {
            parent::setByReferenceWith($value);

            if ($this->analyzer->isArray($this->value) === true) {
                $this->pass();
            } else {
                $this->fail($this->_('%s is not an array', $this));
            }

            return $this;
        }
    }

    public function hasSize($size, $failMessage = null)
    {
        if (count($this->valueIsSet()->value) == $size) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s has size %d, expected size %d', $this, count($this->valueIsSet()->value), $size));
        }

        return $this;
    }

    public function isEmpty($failMessage = null)
    {
        if (count($this->valueIsSet()->value) == 0) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s is not empty', $this));
        }

        return $this;
    }

    public function isNotEmpty($failMessage = null)
    {
        if (count($this->valueIsSet()->value) > 0) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s is empty', $this));
        }

        return $this;
    }

    public function strictlyContains($value, $failMessage = null)
    {
        return $this->containsValue($value, $failMessage, true);
    }

    public function contains($value, $failMessage = null)
    {
        return $this->containsValue($value, $failMessage, false);
    }

    public function strictlyNotContains($value, $failMessage = null)
    {
        return $this->notContainsValue($value, $failMessage, true);
    }

    public function notContains($value, $failMessage = null)
    {
        return $this->notContainsValue($value, $failMessage, false);
    }

    public function atKey($key, $failMessage = null)
    {
        $this->hasKey($key, $failMessage)->key = $key;

        return $this;
    }

    public function hasKeys(array $keys, $failMessage = null)
    {
        if (count($undefinedKeys = array_diff($keys, array_keys($this->valueIsSet()->value))) <= 0) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s has no keys %s', $this, $this->getTypeOf($undefinedKeys)));
        }

        return $this;
    }

    public function notHasKeys(array $keys, $failMessage = null)
    {
        $this->valueIsSet();

        if (count($definedKeys = array_intersect($keys, array_keys($this->value))) <= 0) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s has keys %s', $this, $this->getTypeOf($definedKeys)));
        }

        return $this;
    }

    public function hasKey($key, $failMessage = null)
    {
        if (array_key_exists($key, $this->valueIsSet()->value)) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s has no key %s', $this, $this->getTypeOf($key)));
        }

        return $this;
    }

    public function notHasKey($key, $failMessage = null)
    {
        if (array_key_exists($key, $this->valueIsSet()->value) === false) {
            $this->pass();
        } else {
            $this->fail($failMessage ?: $this->_('%s has key %s', $this, $this->getTypeOf($key)));
        }

        return $this;
    }

    public function containsValues(array $values, $failMessage = null)
    {
        return $this->intersect($values, $failMessage, false);
    }

    public function strictlyContainsValues(array $values, $failMessage = null)
    {
        return $this->intersect($values, $failMessage, true);
    }

    public function notContainsValues(array $values, $failMessage = null)
    {
        return $this->notIntersect($values, $failMessage, false);
    }

    public function strictlyNotContainsValues(array $values, $failMessage = null)
    {
        return $this->notIntersect($values, $failMessage, true);
    }

    public function isEqualTo($value, $failMessage = null)
    {
        return $this->callAssertion(__FUNCTION__, [$value, $failMessage]);
    }

    public function isNotEqualTo($value, $failMessage = null)
    {
        return $this->callAssertion(__FUNCTION__, [$value, $failMessage]);
    }

    public function isIdenticalTo($value, $failMessage = null)
    {
        return $this->callAssertion(__FUNCTION__, [$value, $failMessage]);
    }

    public function isNotIdenticalTo($value, $failMessage = null)
    {
        return $this->callAssertion(__FUNCTION__, [$value, $failMessage]);
    }

    public function isReferenceTo(& $reference, $failMessage = null)
    {
        return $this->callAssertion(__FUNCTION__, [& $reference, $failMessage]);
    }

    protected function containsValue($value, $failMessage, $strict)
    {
        if (in_array($value, $this->valueIsSet()->value, $strict) === true) {
            if ($this->key === null) {
                $this->pass();
            } else {
                if ($strict === false) {
                    $pass = ($this->value[$this->key] == $value);
                } else {
                    $pass = ($this->value[$this->key] === $value);
                }

                if ($pass === false) {
                    $key = $this->key;
                }

                $this->key = null;

                if ($pass === true) {
                    $this->pass();
                } else {
                    if ($failMessage === null) {
                        if ($strict === false) {
                            $failMessage = $this->_('%s does not contain %s at key %s', $this, $this->getTypeOf($value), $this->getTypeOf($key));
                        } else {
                            $failMessage = $this->_('%s does not strictly contain %s at key %s', $this, $this->getTypeOf($value), $this->getTypeOf($key));
                        }
                    }

                    $this->fail($failMessage);
                }
            }
        } else {
            if ($failMessage === null) {
                if ($strict === false) {
                    $failMessage = $this->_('%s does not contain %s', $this, $this->getTypeOf($value));
                } else {
                    $failMessage = $this->_('%s does not strictly contain %s', $this, $this->getTypeOf($value));
                }
            }

            $this->fail($failMessage);
        }

        return $this;
    }

    protected function notContainsValue($value, $failMessage, $strict)
    {
        if (in_array($value, $this->valueIsSet()->value, $strict) === false) {
            $this->pass();
        } else {
            if ($this->key === null) {
                if ($failMessage === null) {
                    if ($strict === false) {
                        $failMessage = $this->_('%s contains %s', $this, $this->getTypeOf($value));
                    } else {
                        $failMessage = $this->_('%s strictly contains %s', $this, $this->getTypeOf($value));
                    }
                }

                $this->fail($failMessage);
            } else {
                if ($strict === false) {
                    $pass = ($this->value[$this->key] != $value);
                } else {
                    $pass = ($this->value[$this->key] !== $value);
                }

                if ($pass === false) {
                    $key = $this->key;
                }

                $this->key = null;

                if ($pass === true) {
                    $this->pass();
                } else {
                    if ($failMessage === null) {
                        if ($strict === false) {
                            $failMessage = $this->_('%s contains %s at key %s', $this, $this->getTypeOf($value), $this->getTypeOf($key));
                        } else {
                            $failMessage = $this->_('%s strictly contains %s at key %s', $this, $this->getTypeOf($value), $this->getTypeOf($key));
                        }
                    }

                    $this->fail($failMessage);
                }
            }
        }

        return $this;
    }

    protected function intersect(array $values, $failMessage, $strict)
    {
        $unknownValues = $this->valueIsSet()->getDifference($values, $strict);

        if (count($unknownValues) === 0) {
            $this->pass();
        } else {
            if ($failMessage === null) {
                if ($strict === false) {
                    $failMessage = $this->_('%s does not contain values %s', $this, $this->getTypeOf($unknownValues));
                } else {
                    $failMessage = $this->_('%s does not contain strictly values %s', $this, $this->getTypeOf($unknownValues));
                }
            }

            $this->fail($failMessage);
        }

        return $this;
    }

    protected function notIntersect(array $values, $failMessage, $strict)
    {
        $knownValues = $this->valueIsSet()->getIntersection($values, $strict);

        if (count($knownValues) === 0) {
            $this->pass();
        } else {
            if ($failMessage === null) {
                if ($strict === false) {
                    $failMessage = $this->_('%s contains values %s', $this, $this->getTypeOf($knownValues));
                } else {
                    $failMessage = $this->_('%s contains strictly values %s', $this, $this->getTypeOf($knownValues));
                }
            }

            $this->fail($failMessage);
        }

        return $this;
    }

    protected function getIntersection(array $values, $strict)
    {
        return $this->getValues($values, true, $strict);
    }

    protected function getDifference(array $values, $strict)
    {
        return $this->getValues($values, false, $strict);
    }

    protected function getValues(array $values, $equal, $strict)
    {
        return array_values(
            array_filter(
                $values,
                function ($value) use ($strict, $equal) {
                    return in_array($value, $this->value, $strict) === $equal;
                }
            )
        );
    }

    protected function valueIsSet($message = 'Array is undefined')
    {
        return parent::valueIsSet($message);
    }

    protected function getKeysAsserter()
    {
        return $this->generator->__call('phpArray', [array_keys($this->valueIsSet()->value)]);
    }

    protected function getValuesAsserter()
    {
        return $this->generator->__call('phpArray', [array_values($this->valueIsSet()->value)]);
    }

    protected function getSizeAsserter()
    {
        return $this->generator->__call('integer', [count($this->valueIsSet()->value)]);
    }

    protected function callAssertion($method, array $arguments)
    {
        if ($this->innerAsserterCanUse($method) === false) {
            call_user_func_array([parent::class, $method], $arguments);
        } else {
            $this->callInnerAsserterMethod($method, $arguments);
        }

        return $this;
    }

    protected function innerAsserterCanUse($method)
    {
        return ($this->innerAsserter !== null && $this->innerValueIsSet === true && method_exists($this->innerAsserter, $method) === true);
    }

    protected function callInnerAsserterMethod($method, $arguments)
    {
        call_user_func_array([$this->innerAsserter->setWith($this->innerValue), $method], $arguments);

        $this->innerAsserterUsed = true;

        return $this;
    }

    protected function resetInnerAsserter()
    {
        $this->innerAsserter = null;
        $this->innerValue = null;
        $this->innerValueIsSet = false;

        return $this;
    }
}
