#include "Slot.h"
#include "../model/DragonBonesData.h"
#include "../model/UserData.h"
#include "../model/SkinData.h"
#include "../model/DisplayData.h"
#include "../model/BoundingBoxData.h"
#include "../model/TextureAtlasData.h"
#include "Armature.h"
#include "Bone.h"
#include "DeformVertices.h"
#include "../animation/Animation.h"
#include "../event/EventObject.h"

DRAGONBONES_NAMESPACE_BEGIN

void Slot::_onClear()
{
    TransformObject::_onClear();

    std::vector<std::pair<void*, DisplayType>> disposeDisplayList;
    for (const auto& pair : this->_displayList)
    {
        if (pair.first != nullptr && pair.first != _rawDisplay && pair.first != _meshDisplay &&
            std::find(disposeDisplayList.cbegin(), disposeDisplayList.cend(), pair) == disposeDisplayList.cend())
        {
            disposeDisplayList.push_back(pair);
        }
    }

    for (const auto& pair : disposeDisplayList)
    {
        if (pair.second == DisplayType::Armature)
        {
            static_cast<Armature*>(pair.first)->returnToPool();
        }
        else
        {
            _disposeDisplay(pair.first, true);
        }
    }

    if (_deformVertices != nullptr)
    {
        _deformVertices->returnToPool();
    }

    if (_meshDisplay && _meshDisplay != _rawDisplay)
    {
        _disposeDisplay(_meshDisplay, false);
    }

    if (_rawDisplay)
    {
        _disposeDisplay(_rawDisplay, false);
    }

    displayController = "";

    _displayDirty          = false;
    _zOrderDirty           = false;
    _blendModeDirty        = false;
    _colorDirty            = false;
    _transformDirty        = false;
    _visible               = true;
    _blendMode             = BlendMode::Normal;
    _displayIndex          = -1;
    _animationDisplayIndex = -1;
    _zOrder                = 0;
    _cachedFrameIndex      = -1;
    _pivotX                = 0.0f;
    _pivotY                = 0.0f;
    _localMatrix.identity();
    _colorTransform.identity();
    _displayList.clear();
    _displayDatas.clear();
    _slotData           = nullptr;
    _rawDisplayDatas    = nullptr;  //
    _displayData        = nullptr;
    _boundingBoxData    = nullptr;
    _textureData        = nullptr;
    _deformVertices     = nullptr;
    _rawDisplay         = nullptr;
    _meshDisplay        = nullptr;
    _display            = nullptr;
    _childArmature      = nullptr;
    _parent             = nullptr;
    _cachedFrameIndices = nullptr;
}

DisplayData* Slot::_getDefaultRawDisplayData(unsigned displayIndex) const
{
    const auto defaultSkin = _armature->_armatureData->defaultSkin;
    if (defaultSkin != nullptr)
    {
        const auto defaultRawDisplayDatas = defaultSkin->getDisplays(_slotData->name);
        if (defaultRawDisplayDatas != nullptr)
        {
            return displayIndex < defaultRawDisplayDatas->size() ? (*defaultRawDisplayDatas)[displayIndex] : nullptr;
        }
    }

    return nullptr;
}

void Slot::_updateDisplayData()
{
    const auto prevDisplayData  = _displayData;
    const auto prevVerticesData = _deformVertices != nullptr ? _deformVertices->verticesData : nullptr;
    const auto prevTextureData  = _textureData;

    DisplayData* rawDisplayData       = nullptr;
    VerticesData* currentVerticesData = nullptr;

    _displayData     = nullptr;
    _boundingBoxData = nullptr;
    _textureData     = nullptr;

    if (_displayIndex >= 0)
    {
        if (_rawDisplayDatas != nullptr)
        {
            rawDisplayData =
                (unsigned)_displayIndex < _rawDisplayDatas->size() ? (*_rawDisplayDatas)[_displayIndex] : nullptr;
        }

        if (rawDisplayData == nullptr)
        {
            rawDisplayData = _getDefaultRawDisplayData(_displayIndex);
        }

        if ((unsigned)_displayIndex < _displayDatas.size())
        {
            _displayData = _displayDatas[_displayIndex];
        }
    }

    // Update texture and mesh data.
    if (_displayData != nullptr)
    {
        if (_displayData->type == DisplayType::Mesh)
        {
            currentVerticesData = &static_cast<MeshDisplayData*>(_displayData)->vertices;
        }
        else if (_displayData->type == DisplayType::Path)
        {
            // TODO
        }
        else if (rawDisplayData != nullptr)
        {
            if (rawDisplayData->type == DisplayType::Mesh)
            {
                currentVerticesData = &static_cast<MeshDisplayData*>(rawDisplayData)->vertices;
            }
            else if (rawDisplayData->type == DisplayType::Path)
            {
                // TODO
            }
        }

        if (_displayData->type == DisplayType::BoundingBox)
        {
            _boundingBoxData = static_cast<BoundingBoxDisplayData*>(_displayData)->boundingBox;
        }
        else if (rawDisplayData != nullptr)
        {
            if (rawDisplayData->type == DisplayType::BoundingBox)
            {
                _boundingBoxData = static_cast<BoundingBoxDisplayData*>(rawDisplayData)->boundingBox;
            }
        }

        if (_displayData->type == DisplayType::Image)
        {
            _textureData = static_cast<ImageDisplayData*>(_displayData)->texture;
        }
        else if (_displayData->type == DisplayType::Mesh)
        {
            _textureData = static_cast<MeshDisplayData*>(_displayData)->texture;
        }
    }

    // Update bounding box data.
    if (_displayData != nullptr && _displayData->type == DisplayType::BoundingBox)
    {
        _boundingBoxData = static_cast<BoundingBoxDisplayData*>(_displayData)->boundingBox;
    }
    else if (rawDisplayData != nullptr && rawDisplayData->type == DisplayType::BoundingBox)
    {
        _boundingBoxData = static_cast<BoundingBoxDisplayData*>(rawDisplayData)->boundingBox;
    }
    else
    {
        _boundingBoxData = nullptr;
    }

    if (_displayData != prevDisplayData || currentVerticesData != prevVerticesData || _textureData != prevTextureData)
    {
        if (currentVerticesData == nullptr && _textureData != nullptr)
        {
            const auto imageDisplayData = static_cast<ImageDisplayData*>(_displayData);
            const auto scale            = _textureData->parent->scale * _armature->_armatureData->scale;
            const auto frame            = _textureData->frame;

            _pivotX = imageDisplayData->pivot.x;
            _pivotY = imageDisplayData->pivot.y;

            const auto& rect = frame != nullptr ? *frame : _textureData->region;
            float width      = rect.width;
            float height     = rect.height;

            if (_textureData->rotated && frame == nullptr)
            {
                width  = rect.height;
                height = rect.width;
            }

            _pivotX *= width * scale;
            _pivotY *= height * scale;

            if (frame != nullptr)
            {
                _pivotX += frame->x * scale;
                _pivotY += frame->y * scale;
            }

            // Update replace pivot.
            if (_displayData != nullptr && rawDisplayData != nullptr && _displayData != rawDisplayData)
            {
                rawDisplayData->transform.toMatrix(_helpMatrix);
                _helpMatrix.invert();
                _helpMatrix.transformPoint(0.0f, 0.0f, _helpPoint);
                _pivotX -= _helpPoint.x;
                _pivotY -= _helpPoint.y;

                _displayData->transform.toMatrix(_helpMatrix);
                _helpMatrix.invert();
                _helpMatrix.transformPoint(0.0f, 0.0f, _helpPoint);
                _pivotX += _helpPoint.x;
                _pivotY += _helpPoint.y;
            }

            if (!DragonBones::yDown)
            {
                _pivotY = (_textureData->rotated ? _textureData->region.width : _textureData->region.height) * scale -
                          _pivotY;
            }
        }
        else
        {
            _pivotX = 0.0f;
            _pivotY = 0.0f;
        }

        // Update original transform.
        if (rawDisplayData != nullptr)
        {
            origin = &rawDisplayData->transform;
        }
        else if (_displayData != nullptr)
        {
            origin = &_displayData->transform;
        }
        else
        {
            origin = nullptr;
        }

        // Update vertices.
        if (currentVerticesData != prevVerticesData)
        {
            if (_deformVertices == nullptr)
            {
                _deformVertices = BaseObject::borrowObject<DeformVertices>();
            }

            _deformVertices->init(currentVerticesData, _armature);
        }
        else if (_deformVertices != nullptr && _textureData != prevTextureData)  // Update mesh after update frame.
        {
            _deformVertices->verticesDirty = true;
        }

        _displayDirty   = true;
        _transformDirty = true;
    }
}

void Slot::_updateDisplay()
{
    const auto prevDisplay       = _display != nullptr ? _display : _rawDisplay;
    const auto prevChildArmature = _childArmature;

    // Update display and child armature.
    if (_displayIndex >= 0 && (std::size_t)_displayIndex < _displayList.size())
    {
        const auto& displayPair = _displayList[_displayIndex];
        _display                = displayPair.first;
        if (_display != nullptr && displayPair.second == DisplayType::Armature)
        {
            _childArmature = static_cast<Armature*>(displayPair.first);
            _display       = _childArmature->getDisplay();
        }
        else
        {
            _childArmature = nullptr;
        }
    }
    else
    {
        _display       = nullptr;
        _childArmature = nullptr;
    }

    const auto currentDisplay = _display != nullptr ? _display : _rawDisplay;
    if (currentDisplay != prevDisplay)
    {
        _onUpdateDisplay();
        _replaceDisplay(prevDisplay, prevChildArmature != nullptr);

        _transformDirty = true;
        _visibleDirty   = true;
        _blendModeDirty = true;
        _colorDirty     = true;
    }

    // Update frame.
    if (currentDisplay == _rawDisplay || currentDisplay == _meshDisplay)
    {
        _updateFrame();
    }

    // Update child armature.
    if (_childArmature != prevChildArmature)
    {
        if (prevChildArmature != nullptr)
        {
            prevChildArmature->_parent = nullptr;  // Update child armature parent.
            prevChildArmature->setClock(nullptr);
            if (prevChildArmature->inheritAnimation)
            {
                prevChildArmature->getAnimation()->reset();
            }
        }

        if (_childArmature != nullptr)
        {
            _childArmature->_parent = this;  // Update child armature parent.
            _childArmature->setClock(_armature->getClock());
            if (_childArmature->inheritAnimation)  // Set child armature cache frameRate.
            {
                if (_childArmature->getCacheFrameRate() == 0)
                {
                    const auto chacheFrameRate = this->_armature->getCacheFrameRate();
                    if (chacheFrameRate != 0)
                    {
                        _childArmature->setCacheFrameRate(chacheFrameRate);
                    }
                }

                // Child armature action.
                std::vector<ActionData*>* actions = nullptr;
                if (_displayData != nullptr && _displayData->type == DisplayType::Armature)
                {
                    actions = &(static_cast<ArmatureDisplayData*>(_displayData)->actions);
                }
                else if (_displayIndex >= 0 && _rawDisplayDatas != nullptr)
                {
                    auto rawDisplayData = (unsigned)_displayIndex < _rawDisplayDatas->size()
                                              ? (*_rawDisplayDatas)[_displayIndex]
                                              : nullptr;

                    if (rawDisplayData == nullptr)
                    {
                        rawDisplayData = _getDefaultRawDisplayData(_displayIndex);
                    }

                    if (rawDisplayData != nullptr && rawDisplayData->type == DisplayType::Armature)
                    {
                        actions = &(static_cast<ArmatureDisplayData*>(rawDisplayData)->actions);
                    }
                }

                if (actions != nullptr && !actions->empty())
                {
                    for (const auto action : *actions)
                    {
                        const auto eventObject = BaseObject::borrowObject<EventObject>();
                        EventObject::actionDataToInstance(action, eventObject, _armature);
                        eventObject->slot = this;
                        _armature->_bufferAction(eventObject, false);
                    }
                }
                else
                {
                    _childArmature->getAnimation()->play();
                }
            }
        }
    }
}

void Slot::_updateGlobalTransformMatrix(bool isCache)
{
    const auto& parentMatrix = _parent->globalTransformMatrix;
    globalTransformMatrix    = _localMatrix;  // Copy.
    globalTransformMatrix.concat(parentMatrix);

    if (isCache)
    {
        global.fromMatrix(globalTransformMatrix);
    }
    else
    {
        _globalDirty = true;
    }
}

bool Slot::_setDisplayIndex(int value, bool isAnimation)
{
    if (isAnimation)
    {
        if (_animationDisplayIndex == value)
        {
            return false;
        }

        _animationDisplayIndex = value;
    }

    if (_displayIndex == value)
    {
        return false;
    }

    _displayIndex = value;
    _displayDirty = true;

    _updateDisplayData();

    return _displayDirty;
}

bool Slot::_setZorder(int value)
{
    if (_zOrder == value)
    {
        // return false;
    }

    _zOrder      = value;
    _zOrderDirty = true;

    return _zOrderDirty;
}

bool Slot::_setColor(const ColorTransform& value)
{
    _colorTransform = value;  // copy
    _colorDirty     = true;

    return true;
}

bool Slot::_setDisplayList(const std::vector<std::pair<void*, DisplayType>>& value)
{
    if (!value.empty())
    {
        if (_displayList.size() != value.size())
        {
            _displayList.resize(value.size());
        }

        for (std::size_t i = 0, l = value.size(); i < l; ++i)
        {
            const auto& eachPair = value[i];
            if (eachPair.first != nullptr && eachPair.first != _rawDisplay && eachPair.first != _meshDisplay &&
                eachPair.second != DisplayType::Armature &&
                std::find(_displayList.cbegin(), _displayList.cend(), eachPair) == _displayList.cend())
            {
                _initDisplay(eachPair.first, true);
            }

            _displayList[i].first  = eachPair.first;
            _displayList[i].second = eachPair.second;
        }
    }
    else if (!_displayList.empty())
    {
        _displayList.clear();
    }

    if (_displayIndex >= 0 && (std::size_t)_displayIndex < _displayList.size())
    {
        _displayDirty = _display != _displayList[_displayIndex].first;
    }
    else
    {
        _displayDirty = _display != nullptr;
    }

    _updateDisplayData();

    return _displayDirty;
}

void Slot::init(const SlotData* slotData, Armature* armatureValue, void* rawDisplay, void* meshDisplay)
{
    if (_slotData != nullptr)
    {
        return;
    }

    _slotData = slotData;
    //
    _visibleDirty   = true;
    _blendModeDirty = true;
    _colorDirty     = true;
    _blendMode      = _slotData->blendMode;
    _zOrder         = _slotData->zOrder;
    _colorTransform = *(_slotData->color);
    _rawDisplay     = rawDisplay;
    _meshDisplay    = meshDisplay;
    //
    _armature = armatureValue;
    //
    const auto slotParent = _armature->getBone(_slotData->parent->name);
    if (slotParent != nullptr)
    {
        _parent = slotParent;
    }
    else
    {
        // Never;
    }

    _armature->_addSlot(this);
    //
    _initDisplay(_rawDisplay, false);
    if (_rawDisplay != _meshDisplay)
    {
        _initDisplay(_meshDisplay, false);
    }

    _onUpdateDisplay();
    _addDisplay();
}

void Slot::update(int cacheFrameIndex)
{
    if (_displayDirty)
    {
        _displayDirty = false;
        _updateDisplay();

        // TODO remove slot offset.
        if (_transformDirty)  // Update local matrix. (Only updated when both display and transform are dirty.)
        {
            if (origin != nullptr)
            {
                global = *origin;  // Copy.
                global.add(offset).toMatrix(_localMatrix);
            }
            else
            {
                global = offset;  // Copy.
                global.toMatrix(_localMatrix);
            }
        }
    }

    if (_zOrderDirty)
    {
        _zOrderDirty = false;
        _updateZOrder();
    }

    if (cacheFrameIndex >= 0 && _cachedFrameIndices != nullptr)
    {
        const auto cachedFrameIndex = (*_cachedFrameIndices)[cacheFrameIndex];
        if (cachedFrameIndex >= 0 && _cachedFrameIndex == cachedFrameIndex)  // Same cache.
        {
            _transformDirty = false;
        }
        else if (cachedFrameIndex >= 0)  // Has been Cached.
        {
            _transformDirty   = true;
            _cachedFrameIndex = cachedFrameIndex;
        }
        else if (_transformDirty || _parent->_childrenTransformDirty)  // Dirty.
        {
            _transformDirty   = true;
            _cachedFrameIndex = -1;
        }
        else if (_cachedFrameIndex >= 0)  // Same cache, but not set index yet.
        {
            _transformDirty                         = false;
            (*_cachedFrameIndices)[cacheFrameIndex] = _cachedFrameIndex;
        }
        else  // Dirty.
        {
            _transformDirty   = true;
            _cachedFrameIndex = -1;
        }
    }
    else if (_transformDirty || this->_parent->_childrenTransformDirty)
    {
        cacheFrameIndex   = -1;
        _transformDirty   = true;
        _cachedFrameIndex = -1;
    }

    if (_display == nullptr)
    {
        return;
    }

    if (_visibleDirty)
    {
        _visibleDirty = false;
        _updateVisible();
    }

    if (_blendModeDirty)
    {
        _blendModeDirty = false;
        _updateBlendMode();
    }

    if (_colorDirty)
    {
        _colorDirty = false;
        _updateColor();
    }

    if (_deformVertices != nullptr && _deformVertices->verticesData != nullptr && _display == _meshDisplay)
    {
        const auto isSkinned = _deformVertices->verticesData->weight != nullptr;

        if (_deformVertices->verticesDirty || (isSkinned && _deformVertices->isBonesUpdate()))
        {
            _deformVertices->verticesDirty = false;
            _updateMesh();
        }

        if (isSkinned)  // Compatible.
        {
            return;
        }
    }

    if (_transformDirty)
    {
        _transformDirty = false;

        if (_cachedFrameIndex < 0)
        {
            const auto isCache = cacheFrameIndex >= 0;
            _updateGlobalTransformMatrix(isCache);

            if (isCache && _cachedFrameIndices != nullptr)
            {
                _cachedFrameIndex = (*_cachedFrameIndices)[cacheFrameIndex] =
                    _armature->_armatureData->setCacheFrame(globalTransformMatrix, global);
            }
        }
        else
        {
            _armature->_armatureData->getCacheFrame(globalTransformMatrix, global, _cachedFrameIndex);
        }

        _updateTransform();
    }
}

void Slot::updateTransformAndMatrix()
{
    if (_transformDirty)
    {
        _transformDirty = false;
        _updateGlobalTransformMatrix(false);
    }
}

void Slot::replaceDisplayData(DisplayData* displayData, int displayIndex)
{
    if (displayIndex < 0)
    {
        if (_displayIndex < 0)
        {
            displayIndex = 0;
        }
        else
        {
            displayIndex = _displayIndex;
        }
    }

    if (_displayDatas.size() <= (unsigned)displayIndex)
    {
        _displayDatas.resize(displayIndex + 1, nullptr);
    }

    _displayDatas[displayIndex] = displayData;
}

bool Slot::containsPoint(float x, float y)
{
    if (_boundingBoxData == nullptr)
    {
        return false;
    }

    updateTransformAndMatrix();

    _helpMatrix = globalTransformMatrix;  // Copy.
    _helpMatrix.invert();
    _helpMatrix.transformPoint(x, y, _helpPoint);

    return _boundingBoxData->containsPoint(_helpPoint.x, _helpPoint.y);
}

int Slot::intersectsSegment(float xA,
                            float yA,
                            float xB,
                            float yB,
                            Point* intersectionPointA,
                            Point* intersectionPointB,
                            Point* normalRadians)
{
    if (_boundingBoxData == nullptr)
    {
        return 0;
    }

    updateTransformAndMatrix();
    _helpMatrix = globalTransformMatrix;
    _helpMatrix.invert();
    _helpMatrix.transformPoint(xA, yA, _helpPoint);
    xA = _helpPoint.x;
    yA = _helpPoint.y;
    _helpMatrix.transformPoint(xB, yB, _helpPoint);
    xB = _helpPoint.x;
    yB = _helpPoint.y;

    const auto intersectionCount =
        _boundingBoxData->intersectsSegment(xA, yA, xB, yB, intersectionPointA, intersectionPointB, normalRadians);
    if (intersectionCount > 0)
    {
        if (intersectionCount == 1 || intersectionCount == 2)
        {
            if (intersectionPointA != nullptr)
            {
                globalTransformMatrix.transformPoint(intersectionPointA->x, intersectionPointA->y, *intersectionPointA);
                if (intersectionPointB != nullptr)
                {
                    intersectionPointB->x = intersectionPointA->x;
                    intersectionPointB->y = intersectionPointA->y;
                }
            }
            else if (intersectionPointB != nullptr)
            {
                globalTransformMatrix.transformPoint(intersectionPointB->x, intersectionPointB->y, *intersectionPointB);
            }
        }
        else
        {
            if (intersectionPointA != nullptr)
            {
                globalTransformMatrix.transformPoint(intersectionPointA->x, intersectionPointA->y, *intersectionPointA);
            }

            if (intersectionPointB != nullptr)
            {
                globalTransformMatrix.transformPoint(intersectionPointB->x, intersectionPointB->y, *intersectionPointB);
            }
        }

        if (normalRadians != nullptr)
        {
            globalTransformMatrix.transformPoint(cos(normalRadians->x), sin(normalRadians->x), _helpPoint, true);
            normalRadians->x = atan2(_helpPoint.y, _helpPoint.x);

            globalTransformMatrix.transformPoint(cos(normalRadians->y), sin(normalRadians->y), _helpPoint, true);
            normalRadians->y = atan2(_helpPoint.y, _helpPoint.x);
        }
    }

    return intersectionCount;
}

void Slot::setVisible(bool value)
{
    if (_visible == value)
    {
        return;
    }

    _visible = value;
    _updateVisible();
}

void Slot::setDisplayIndex(int value)
{
    if (_setDisplayIndex(value))
    {
        update(-1);
    }
}

// TODO lsc check
void Slot::setDisplayList(const std::vector<std::pair<void*, DisplayType>>& value)
{
    const auto backupDisplayList = _displayList;       // copy
    auto disposeDisplayList      = backupDisplayList;  // copy
    disposeDisplayList.clear();

    if (_setDisplayList(value))
    {
        update(-1);
    }

    for (const auto& pair : backupDisplayList)
    {
        if (pair.first != nullptr && pair.first != _rawDisplay && pair.first != _meshDisplay &&
            std::find(_displayList.cbegin(), _displayList.cend(), pair) == _displayList.cend() &&
            std::find(disposeDisplayList.cbegin(), disposeDisplayList.cend(), pair) == disposeDisplayList.cend())
        {
            disposeDisplayList.push_back(pair);
        }
    }

    for (const auto& pair : disposeDisplayList)
    {
        if (pair.second == DisplayType::Armature)
        {
            static_cast<Armature*>(pair.first)->returnToPool();
        }
        else
        {
            _disposeDisplay(pair.first, true);
        }
    }
}

void Slot::setRawDisplayDatas(const std::vector<DisplayData*>* value)
{
    if (_rawDisplayDatas == value)
    {
        return;
    }

    _displayDirty    = true;
    _rawDisplayDatas = value;

    if (_rawDisplayDatas != nullptr)
    {
        _displayDatas.resize(_rawDisplayDatas->size());

        for (std::size_t i = 0, l = _displayDatas.size(); i < l; ++i)
        {
            auto rawDisplayData = (*_rawDisplayDatas)[i];

            if (rawDisplayData == nullptr)
            {
                rawDisplayData = _getDefaultRawDisplayData(i);
            }

            _displayDatas[i] = rawDisplayData;
        }
    }
    else
    {
        _displayDatas.clear();
    }
}

void Slot::setDisplay(void* value, DisplayType displayType)
{
    if (_display == value)
    {
        return;
    }

    const auto displayListLength = _displayList.size();
    if (_displayIndex < 0 && displayListLength == 0)  // Emprty
    {
        _displayIndex = 0;
    }

    if (_displayIndex < 0)
    {
        return;
    }
    else
    {
        auto relpaceDisplayList = _displayList;  // copy
        if (displayListLength <= (std::size_t)_displayIndex)
        {
            relpaceDisplayList.resize(_displayIndex + 1);
        }

        relpaceDisplayList[_displayIndex].first  = value;
        relpaceDisplayList[_displayIndex].second = displayType;

        setDisplayList(relpaceDisplayList);
    }
}

void Slot::setChildArmature(Armature* value)
{
    if (_childArmature == value)
    {
        return;
    }

    setDisplay(value, DisplayType::Armature);
}

DRAGONBONES_NAMESPACE_END