#pragma once

#include "classicmode/classicnote.h"
#include "graphics/classicgraphicsmanager.h"
#include "graphics/classicgraphicsfactory.h"
#include "core/timeline.h"
#include "core/spritecontainer.h"

#include "editor/mockelement.h"
#include "game/arrowelement.h"

#include "graphics/animations/classicflyinganimationscenario.h"
#include "graphics/animations/classicdyinganimationscenario.h"

template <class TNote, class = std::enable_if_t<std::is_base_of<kku::Note, TNote>::value>>
class ClassicSceneGraphicsManager : public ClassicGraphicsManager
{
public:
    explicit ClassicSceneGraphicsManager(const std::shared_ptr<kku::Timeline<TNote>>& timeline,
                                         const std::shared_ptr<ClassicGraphicsFactory>& factory,
                                         const kku::microsec& visibility_offset) :
        ClassicGraphicsManager(visibility_offset),
        _sprite_container({Type::UP, Type::DOWN,
                          Type::LEFT, Type::RIGHT},
                          factory),
        _factory(factory),
        _timeline(timeline)
    {
        _timeline->expire(_first);
        _timeline->expire(_last);
    }

    virtual void input(kku::GameEvent&& input) override
    {
        if (nothingToDraw())
            return;

        for (auto it = _first; it != _last; ++it)
        {
            (*it)->input(std::move(input));
        }
    }

    virtual void display() const override
    {
        if (nothingToDraw())
            return;

        for (auto it = _first; it != _last; ++it)
        {
            //display((*it)->getElements());
        }
    }

    virtual void update(const kku::microsec& offset) override
    {
        fetchLastNote(offset);
        fetchFirstNote(offset);

        updateVisibleNotes(offset);
    }

    virtual void display(const std::vector<ArrowElement>& elements) const
    {
        for (std::size_t i = 0; i < elements.size(); ++i)
        {
            const auto& sprite = elements[i].sprite;

            if (i >= 1)
            {
                //const auto& neighbor_sprite = elements[i - 1].sprite;

                //const auto c1 = neighbor_sprite->trailPosition();
                //const auto c2 = sprite->trailPosition();

                //_render_target->draw(makeLine(c1, c2));
            }

            sprite->display();
        }
    }

    virtual void setGraphics(std::vector<ArrowElement>& elements, kku::TimeRange&& range)
    {
        for (auto& element : elements)
        {
            element.sprite = _sprite_container.getSprite(element.type);
            element.sprite->setPosition(element.position);
            element.sprite->setTrailPosition(kku::Point( 0.f, 9.f ));

            element.animations[ClassicNote::State::NONE] = nullptr;
            element.animations[ClassicNote::State::FLYING] = std::make_shared<ClassicFlyingAnimationScenario>();
            element.animations[ClassicNote::State::DYING] = std::make_shared<ClassicDyingAnimationScenario>();
            element.animations[ClassicNote::State::DEAD] = nullptr;

            element.animations[ClassicNote::State::FLYING]->launch(element.sprite, range.begin, range.end);
        }
    }

    virtual void removeGraphics(std::vector<ArrowElement>& elements)
    {
        for (auto& element : elements)
        {
            _sprite_container.resetSprite(element.sprite, element.type);
            element.sprite = nullptr;

            element.animations[ClassicNote::State::NONE] = nullptr;
            element.animations[ClassicNote::State::FLYING] = nullptr;
            element.animations[ClassicNote::State::DYING] = nullptr;
            element.animations[ClassicNote::State::DEAD] = nullptr;
        }
    }

    virtual void display(const std::vector<MockElement>& elements) const
    {
        for (std::size_t i = 0; i < elements.size(); ++i)
        {
            const auto& sprite = elements[i].sprite;

            if (i >= 1)
            {
                //const auto& neighbor_sprite = elements[i - 1].sprite;

                //const auto c1 = neighbor_sprite->trailPosition();
                //const auto c2 = sprite->trailPosition();

                //_render_target->draw(makeLine(c1, c2));
            }

            sprite->display();
        }
    }

    virtual void setGraphics(std::vector<MockElement>& elements, kku::TimeRange&& range)
    {
        for (auto& element : elements)
        {
            element.sprite = _sprite_container.getSprite(element.type);
            element.sprite->setPosition(element.position);
            element.sprite->setTrailPosition(kku::Point( 0.f, 9.f ));

            //element.selection = _factory->createSelection();
            //element.selection->adjustTo(element.sprite);

            element.animations[ClassicNote::State::NONE] = nullptr;
            element.animations[ClassicNote::State::FLYING] = std::make_shared<ClassicFlyingAnimationScenario>();
            element.animations[ClassicNote::State::DYING] = std::make_shared<ClassicDyingAnimationScenario>();
            element.animations[ClassicNote::State::DEAD] = nullptr;
            element.animations[ClassicNote::State::FLYING]->launch(element.sprite, range.begin, range.end);
        }
    }

    virtual void removeGraphics(std::vector<MockElement>& elements)
    {
        for (auto& element : elements)
        {
            _sprite_container.resetSprite(element.sprite, element.type);
            element.sprite = nullptr;

            element.animations[ClassicNote::State::NONE] = nullptr;
            element.animations[ClassicNote::State::FLYING] = nullptr;
            element.animations[ClassicNote::State::DYING] = nullptr;
            element.animations[ClassicNote::State::DEAD] = nullptr;
        }
    }

protected:
    kku::SpriteContainer<Type, ClassicGraphicsFactory, ClassicNoteGraphics> _sprite_container;
    const std::shared_ptr<const ClassicGraphicsFactory> _factory;

    typedef typename std::set<TNote*>::const_iterator Iterator;

    Iterator _first;
    Iterator _last;

    const std::shared_ptr<kku::Timeline<TNote>> _timeline;

    inline bool nothingToDraw() const noexcept
    {
        return _timeline->isExpired(_first)
            || _timeline->isExpired(_last);
    }

    inline bool isVisiblyClose(const Iterator& iterator, const kku::microsec& music_offset) const noexcept
    {
        return ((*iterator)->getPerfectOffset() - _visibility_offset) <= music_offset;
    }

    void fetchFirstNote(const kku::microsec& offset)
    {
        if (nothingToDraw())
            return;

        if (offset < (*_first)->getPerfectOffset())
        {
            Iterator note_iterator = _first;
            while (note_iterator != _timeline->begin() && isVisiblyClose(note_iterator, offset))
            {
                --note_iterator;
            }

            _first = note_iterator;

            auto note = *_first;
            if (note->getState() != ClassicNote::State::FLYING
            &&  note->getState() != ClassicNote::State::DYING
            &&  offset <= note->getPerfectOffset())
            {
                note->setState(ClassicNote::State::FLYING);
                //setGraphics(note, kku::TimeRange{note->getPerfectOffset() - _visibility_offset, note->getPerfectOffset()});
            }
        }
        else
        {
            Iterator note_iterator = _first;
            while (note_iterator != _last)
            {
                auto note = *note_iterator;
                if (note->getState() == ClassicNote::State::DEAD)
                {
                   // note->removeGraphics(this);
                    ++_first;
                }

                ++note_iterator;
            }
        }
    }

    void fetchLastNote(const kku::microsec& offset)
    {
        Iterator note_iterator = _timeline->getTopNote();
        while (!_timeline->isExpired(note_iterator) && isVisiblyClose(note_iterator, offset))
        {
            if (nothingToDraw())
                _first = note_iterator;

            auto note = *note_iterator;

            if (note->getState() != ClassicNote::State::FLYING
            &&  note->getState() != ClassicNote::State::DYING
            &&  offset <= note->getPerfectOffset())
            {
                note->setState(ClassicNote::State::FLYING);
                //note->setGraphics(this, kku::TimeRange{note->getPerfectOffset() - _visibility_offset, note->getPerfectOffset()});
            }

            ++note_iterator;
        }

        _last = note_iterator;
    }

    void updateVisibleNotes(const kku::microsec& offset)
    {
        for (auto it = _first; it != _last; ++it)
            (*it)->update(offset);
    }
};
