#include "Base.h"
#include "Form.h"
#include "AbsoluteLayout.h"
#include "FlowLayout.h"
#include "VerticalLayout.h"
#include "Game.h"
#include "Theme.h"
#include "Label.h"
#include "Button.h"
#include "CheckBox.h"
#include "Scene.h"

#define FORM_VSH \
    "uniform mat4 u_worldViewProjectionMatrix;\n" \
    "attribute vec3 a_position;\n" \
    "attribute vec2 a_texCoord;\n" \
    "varying vec2 v_texCoord;\n" \
    "void main()\n" \
    "{\n" \
        "gl_Position = u_worldViewProjectionMatrix * vec4(a_position, 1);\n" \
        "v_texCoord = a_texCoord;\n" \
    "}\n"

#define FORM_FSH \
    "#ifdef OPENGL_ES\n" \
    "precision highp float;\n" \
    "#endif\n" \
    "varying vec2 v_texCoord;\n" \
    "uniform sampler2D u_texture;\n" \
    "void main()\n" \
    "{\n" \
        "gl_FragColor = texture2D(u_texture, v_texCoord);\n" \
    "}\n"

namespace gameplay
{

static Effect* __formEffect = NULL;
static std::vector<Form*> __forms;

Form::Form() : _theme(NULL), _frameBuffer(NULL), _spriteBatch(NULL), _node(NULL), _nodeQuad(NULL), _nodeMaterial(NULL) , _u2(0), _v1(0)
{
}

Form::~Form()
{
    SAFE_RELEASE(_node);
    SAFE_DELETE(_spriteBatch);
    SAFE_RELEASE(_frameBuffer);
    SAFE_RELEASE(_theme);

    if (__formEffect)
    {
        if (__formEffect->getRefCount() == 1)
        {
            __formEffect->release();
            __formEffect = NULL;
        }
    }

    // Remove this Form from the global list.
    std::vector<Form*>::iterator it = std::find(__forms.begin(), __forms.end(), this);
    if (it != __forms.end())
    {
        __forms.erase(it);
    }
}

Form* Form::create(const char* id, Theme::Style* style, Layout::Type layoutType)
{
    GP_ASSERT(style);

    Layout* layout;
    switch (layoutType)
    {
    case Layout::LAYOUT_ABSOLUTE:
        layout = AbsoluteLayout::create();
        break;
    case Layout::LAYOUT_FLOW:
        layout = FlowLayout::create();
        break;
    case Layout::LAYOUT_VERTICAL:
        layout = VerticalLayout::create();
        break;
    default:
        GP_ERROR("Unsupported layout type '%d'.", layoutType);
        break;
    }

    Form* form = new Form();
    if (id)
        form->_id = id;
    form->_style = style;
    form->_layout = layout;
    form->_theme = style->getTheme();
    form->_theme->addRef();

    // Get default projection matrix.
    Game* game = Game::getInstance();
    Matrix::createOrthographicOffCenter(0, game->getWidth(), game->getHeight(), 0, 0, 1, &form->_defaultProjectionMatrix);

    __forms.push_back(form);

    return form;
}

Form* Form::create(const char* url)
{
    // Load Form from .form file.
    Properties* properties = Properties::create(url);
    if (properties == NULL)
    {
        GP_ASSERT(properties);
        return NULL;
    }

    // Check if the Properties is valid and has a valid namespace.
    Properties* formProperties = (strlen(properties->getNamespace()) > 0) ? properties : properties->getNextNamespace();
    assert(formProperties);
    if (!formProperties || !(strcmp(formProperties->getNamespace(), "form") == 0))
    {
        GP_ASSERT(formProperties);
        SAFE_DELETE(properties);
        return NULL;
    }

    // Create new form with given ID, theme and layout.
    const char* themeFile = formProperties->getString("theme");
    const char* layoutString = formProperties->getString("layout");
        
    Layout* layout;
    switch (getLayoutType(layoutString))
    {
    case Layout::LAYOUT_ABSOLUTE:
        layout = AbsoluteLayout::create();
        break;
    case Layout::LAYOUT_FLOW:
        layout = FlowLayout::create();
        break;
    case Layout::LAYOUT_VERTICAL:
        layout = VerticalLayout::create();
        break;
    default:
        GP_ERROR("Unsupported layout type '%d'.", getLayoutType(layoutString));
        break;
    }

    Theme* theme = Theme::create(themeFile);
    GP_ASSERT(theme);

    Form* form = new Form();
    form->_layout = layout;
    form->_theme = theme;

    // Get default projection matrix.
    Game* game = Game::getInstance();
    Matrix::createOrthographicOffCenter(0, game->getWidth(), game->getHeight(), 0, 0, 1, &form->_defaultProjectionMatrix);

    Theme::Style* style = NULL;
    const char* styleName = formProperties->getString("style");
    if (styleName)
    {
        style = theme->getStyle(styleName);
    }
    else
    {
        style = theme->getEmptyStyle();
    }
    form->initialize(style, formProperties);

    form->_consumeInputEvents = formProperties->getBool("consumeInputEvents", true);

    // Alignment
    if ((form->_alignment & Control::ALIGN_BOTTOM) == Control::ALIGN_BOTTOM)
    {
        form->_bounds.y = Game::getInstance()->getHeight() - form->_bounds.height;
    }
    else if ((form->_alignment & Control::ALIGN_VCENTER) == Control::ALIGN_VCENTER)
    {
        form->_bounds.y = Game::getInstance()->getHeight() * 0.5f - form->_bounds.height * 0.5f;
    }

    if ((form->_alignment & Control::ALIGN_RIGHT) == Control::ALIGN_RIGHT)
    {
        form->_bounds.x = Game::getInstance()->getWidth() - form->_bounds.width;
    }
    else if ((form->_alignment & Control::ALIGN_HCENTER) == Control::ALIGN_HCENTER)
    {
        form->_bounds.x = Game::getInstance()->getWidth() * 0.5f - form->_bounds.width * 0.5f;
    }

    form->_scroll = getScroll(formProperties->getString("scroll"));
    form->_scrollBarsAutoHide = formProperties->getBool("scrollBarsAutoHide");
    if (form->_scrollBarsAutoHide)
    {
        form->_scrollBarOpacity = 0.0f;
    }

    // Add all the controls to the form.
    form->addControls(theme, formProperties);

    SAFE_DELETE(properties);

    __forms.push_back(form);

    return form;
}

Form* Form::getForm(const char* id)
{
    std::vector<Form*>::const_iterator it;
    for (it = __forms.begin(); it < __forms.end(); it++)
    {
        Form* f = *it;
        GP_ASSERT(f);
        if (strcmp(id, f->getId()) == 0)
        {
            return f;
        }
    }
    return NULL;
}

Theme* Form::getTheme() const
{
    return _theme;
}

void Form::setSize(float width, float height)
{
    if (_autoWidth)
    {
        width = Game::getInstance()->getWidth();
    }

    if (_autoHeight)
    {
        height = Game::getInstance()->getHeight();
    }

    if (width != 0.0f && height != 0.0f &&
        (width != _bounds.width || height != _bounds.height))
    {
        // Width and height must be powers of two to create a texture.
        unsigned int w = nextPowerOfTwo(width);
        unsigned int h = nextPowerOfTwo(height);
        _u2 = width / (float)w;
        _v1 = height / (float)h;

        // Create framebuffer if necessary.
        if (_frameBuffer)
            SAFE_RELEASE(_frameBuffer)
        
        _frameBuffer = FrameBuffer::create(_id.c_str(), w, h);
        GP_ASSERT(_frameBuffer);

        // Re-create projection matrix.
        Matrix::createOrthographicOffCenter(0, width, height, 0, 0, 1, &_projectionMatrix);

        // Re-create sprite batch.
        SAFE_DELETE(_spriteBatch);
        _spriteBatch = SpriteBatch::create(_frameBuffer->getRenderTarget()->getTexture());
        GP_ASSERT(_spriteBatch);

        // Clear the framebuffer black
        Game* game = Game::getInstance();
        _frameBuffer->bind();
        Rectangle prevViewport = game->getViewport();
        game->setViewport(Rectangle(0, 0, width, height));
        _theme->setProjectionMatrix(_projectionMatrix);
        game->clear(Game::CLEAR_COLOR, Vector4::zero(), 1.0, 0);
        _theme->setProjectionMatrix(_defaultProjectionMatrix);
        FrameBuffer::bindDefault();
        game->setViewport(prevViewport);
    }
    
    _bounds.width = width;
    _bounds.height = height;
    _dirty = true;
}

void Form::setBounds(const Rectangle& bounds)
{
    setPosition(bounds.x, bounds.y);
    setSize(bounds.width, bounds.height);
}

void Form::setWidth(float width)
{
    setSize(width, _bounds.height);
}

void Form::setHeight(float height)
{
    setSize(_bounds.width, height);
}

void Form::setAutoWidth(bool autoWidth)
{
    if (_autoWidth != autoWidth)
    {
        _autoWidth = autoWidth;
        _dirty = true;

        if (_autoWidth)
        {
            setSize(_bounds.width, Game::getInstance()->getWidth());
        }
    }
}

void Form::setAutoHeight(bool autoHeight)
{
    if (_autoHeight != autoHeight)
    {
        _autoHeight = autoHeight;
        _dirty = true;

        if (_autoHeight)
        {
            setSize(_bounds.width, Game::getInstance()->getHeight());
        }
    }
}

static Effect* createEffect()
{
    Effect* effect = NULL;
    if (__formEffect == NULL)
    {
        __formEffect = Effect::createFromSource(FORM_VSH, FORM_FSH);
        if (__formEffect == NULL)
        {
            GP_ERROR("Unable to load form effect.");
            return NULL;
        }
        effect = __formEffect;
    }
    else
    {
        effect = __formEffect;
    }
    return effect;
}

void Form::setNode(Node* node)
{
    // If the user wants a custom node then we need to create a 3D quad
    if (node && node != _node)
    {
        // Set this Form up to be 3D by initializing a quad.
        float x2 = _bounds.width;
        float y2 = _bounds.height;
        float vertices[] =
        {
            0, y2, 0,   0, _v1,
            0, 0, 0,    0, 0,
            x2, y2, 0,  _u2, _v1,
            x2, 0, 0,   _u2, 0
        };
        VertexFormat::Element elements[] =
        {
            VertexFormat::Element(VertexFormat::POSITION, 3),
            VertexFormat::Element(VertexFormat::TEXCOORD0, 2)
        };
        Mesh* mesh = Mesh::createMesh(VertexFormat(elements, 2), 4, false);
        GP_ASSERT(mesh);
        mesh->setPrimitiveType(Mesh::TRIANGLE_STRIP);
        mesh->setVertexData(vertices, 0, 4);

        _nodeQuad = Model::create(mesh);
        SAFE_RELEASE(mesh);
        GP_ASSERT(_nodeQuad);

        // Create the effect and material
        Effect* effect = createEffect();
        GP_ASSERT(effect);
        _nodeMaterial = Material::create(effect);

        GP_ASSERT(_nodeMaterial);
        _nodeQuad->setMaterial(_nodeMaterial);
        _nodeMaterial->release();
        node->setModel(_nodeQuad);
        _nodeQuad->release();

        // Bind the WorldViewProjection matrix.
        _nodeMaterial->setParameterAutoBinding("u_worldViewProjectionMatrix", RenderState::WORLD_VIEW_PROJECTION_MATRIX);

        // Bind the texture from the framebuffer and set the texture to clamp
        Texture::Sampler* sampler = Texture::Sampler::create(_frameBuffer->getRenderTarget()->getTexture());
        GP_ASSERT(sampler);
        sampler->setWrapMode(Texture::CLAMP, Texture::CLAMP);
        _nodeMaterial->getParameter("u_texture")->setValue(sampler);
        sampler->release();

        RenderState::StateBlock* rsBlock = _nodeMaterial->getStateBlock();
        rsBlock->setDepthWrite(true);
        rsBlock->setBlend(true);
        rsBlock->setBlendSrc(RenderState::BLEND_SRC_ALPHA);
        rsBlock->setBlendDst(RenderState::BLEND_ONE_MINUS_SRC_ALPHA);
    }
    _node = node;
}

void Form::update(float elapsedTime)
{
    if (isDirty())
    {
        _clearBounds.set(_absoluteClipBounds);

        // Calculate the clipped bounds.
        float x = 0;
        float y = 0;
        float width = _bounds.width;
        float height = _bounds.height;

        Rectangle clip(0, 0, _bounds.width, _bounds.height);

        float clipX2 = clip.x + clip.width;
        float x2 = clip.x + x + width;
        if (x2 > clipX2)
            width -= x2 - clipX2;

        float clipY2 = clip.y + clip.height;
        float y2 = clip.y + y + height;
        if (y2 > clipY2)
            height -= y2 - clipY2;

        if (x < 0)
        {
            width += x;
            x = -x;
        }
        else
        {
            x = 0;
        }

        if (y < 0)
        {
            height += y;
            y = -y;
        }
        else
        {
            y = 0;
        }

        _clipBounds.set(x, y, width, height);

        // Calculate the absolute bounds.
        x = 0;
        y = 0;
        _absoluteBounds.set(x, y, _bounds.width, _bounds.height);

        // Calculate the absolute viewport bounds. Absolute bounds minus border and padding.
        const Theme::Border& border = getBorder(_state);
        const Theme::Padding& padding = getPadding();

        x += border.left + padding.left;
        y += border.top + padding.top;
        width = _bounds.width - border.left - padding.left - border.right - padding.right;
        height = _bounds.height - border.top - padding.top - border.bottom - padding.bottom;

        _viewportBounds.set(x, y, width, height);

        // Calculate the clip area. Absolute bounds, minus border and padding, clipped to the parent container's clip area.
        clipX2 = clip.x + clip.width;
        x2 = x + width;
        if (x2 > clipX2)
            width = clipX2 - x;

        clipY2 = clip.y + clip.height;
        y2 = y + height;
        if (y2 > clipY2)
            height = clipY2 - y;

        if (x < clip.x)
        {
            float dx = clip.x - x;
            width -= dx;
            x = clip.x;
        }

        if (y < clip.y)
        {
            float dy = clip.y - y;
            height -= dy;
            y = clip.y;
        }
 
        _viewportClipBounds.set(x, y, width, height);

        _absoluteClipBounds.set(x - border.left - padding.left, y - border.top - padding.top,
            width + border.left + padding.left + border.right + padding.right,
            height + border.top + padding.top + border.bottom + padding.bottom);
        if (_clearBounds.isEmpty())
        {
            _clearBounds.set(_absoluteClipBounds);
        }

        // Cache themed attributes for performance.
        _skin = getSkin(_state);
        _opacity = getOpacity(_state);

        // Get scrollbar images and diminish clipping bounds to make room for scrollbars.
        if ((_scroll & SCROLL_HORIZONTAL) == SCROLL_HORIZONTAL)
        {
            _scrollBarLeftCap = getImage("scrollBarLeftCap", _state);
            _scrollBarHorizontal = getImage("horizontalScrollBar", _state);
            _scrollBarRightCap = getImage("scrollBarRightCap", _state);

            _viewportClipBounds.height -= _scrollBarHorizontal->getRegion().height;
        }

        if ((_scroll & SCROLL_VERTICAL) == SCROLL_VERTICAL)
        {
            _scrollBarTopCap = getImage("scrollBarTopCap", _state);
            _scrollBarVertical = getImage("verticalScrollBar", _state);
            _scrollBarBottomCap = getImage("scrollBarBottomCap", _state);
        
            _viewportClipBounds.width -= _scrollBarVertical->getRegion().width;
        }

        GP_ASSERT(_layout);
        if (_scroll != SCROLL_NONE)
            updateScroll();
        else
            _layout->update(this, Vector2::zero());
    }
}

void Form::draw()
{
    // The first time a form is drawn, its contents are rendered into a framebuffer.
    // The framebuffer will only be drawn into again when the contents of the form change.
    // If this form has a node then it's a 3D form and the framebuffer will be used
    // to texture a quad.  The quad will be given the same dimensions as the form and
    // must be transformed appropriately by the user, unless they call setQuad() themselves.
    // On the other hand, if this form has not been set on a node, SpriteBatch will be used
    // to render the contents of the framebuffer directly to the display.

    // Check whether this form has changed since the last call to draw() and if so, render into the framebuffer.
    if (isDirty())
    {
        GP_ASSERT(_frameBuffer);
        _frameBuffer->bind();

        Game* game = Game::getInstance();
        Rectangle prevViewport = game->getViewport();
        game->setViewport(Rectangle(0, 0, _bounds.width, _bounds.height));

        GP_ASSERT(_theme);
        _theme->setProjectionMatrix(_projectionMatrix);
        Container::draw(_theme->getSpriteBatch(), Rectangle(0, 0, _bounds.width, _bounds.height), _skin != NULL, false, _bounds.height);
        _theme->setProjectionMatrix(_defaultProjectionMatrix);

        // Rebind the default framebuffer and game viewport.
        FrameBuffer::bindDefault();

        // restore the previous game viewport
        game->setViewport(prevViewport);
    }

    // Draw either with a 3D quad or sprite batch
    if (_node)
    {
         // If we have the node set, then draw a 3D quad model
        _nodeQuad->draw();
    }
    else
    {
        // Otherwise we draw the framebuffer in ortho space with a spritebatch.
        if (!_spriteBatch)
        {
            _spriteBatch = SpriteBatch::create(_frameBuffer->getRenderTarget()->getTexture());
            GP_ASSERT(_spriteBatch);
        }
        _spriteBatch->start();
        _spriteBatch->draw(_bounds.x, _bounds.y, 0, _bounds.width, _bounds.height, 0, _v1, _u2, 0, Vector4::one());
        _spriteBatch->finish();
    }
}

const char* Form::getType() const
{
    return "form";
}

bool Form::touchEventInternal(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex)
{
    // Check for a collision with each Form in __forms.
    // Pass the event on.
    bool eventConsumed = false;
    size_t size = __forms.size();
    for (size_t i = 0; i < size; ++i)
    {
        Form* form = __forms[i];
        GP_ASSERT(form);

        if (form->isEnabled())
        {
            if (form->_node)
            {
                Vector3 point;
                if (form->projectPoint(x, y, &point))
                {
                    const Rectangle& bounds = form->getBounds();
                    if (form->getState() == Control::FOCUS ||
                        (evt == Touch::TOUCH_PRESS &&
                         point.x >= bounds.x &&
                         point.x <= bounds.x + bounds.width &&
                         point.y >= bounds.y &&
                         point.y <= bounds.y + bounds.height))
                    {
                        eventConsumed |= form->touchEvent(evt, point.x - bounds.x, bounds.height - point.y - bounds.y, contactIndex);
                    }
                }
            }
            else
            {
                // Simply compare with the form's bounds.
                const Rectangle& bounds = form->getBounds();
                if (form->getState() == Control::FOCUS ||
                    (evt == Touch::TOUCH_PRESS &&
                        x >= bounds.x &&
                        x <= bounds.x + bounds.width &&
                        y >= bounds.y &&
                        y <= bounds.y + bounds.height))
                {
                    // Pass on the event's position relative to the form.
                    eventConsumed |= form->touchEvent(evt, x - bounds.x, y - bounds.y, contactIndex);
                }
            }
        }
    }
    return eventConsumed;
}

bool Form::keyEventInternal(Keyboard::KeyEvent evt, int key)
{
    size_t size = __forms.size();
    for (size_t i = 0; i < size; ++i)
    {
        Form* form = __forms[i];
        GP_ASSERT(form);
        if (form->isEnabled())
        {
            if (form->keyEvent(evt, key))
                return true;
        }
    }
    return false;
}

bool Form::mouseEventInternal(Mouse::MouseEvent evt, int x, int y, int wheelDelta)
{
    bool eventConsumed = false;

    for (size_t i = 0; i < __forms.size(); ++i)
    {
        Form* form = __forms[i];
        GP_ASSERT(form);

        if (form->isEnabled())
        {
            if (form->_node)
            {
                Vector3 point;
                if (form->projectPoint(x, y, &point))
                {
                    const Rectangle& bounds = form->getBounds();
                    if (form->getState() == Control::FOCUS ||
                        ((evt == Mouse::MOUSE_PRESS_LEFT_BUTTON ||
                          evt == Mouse::MOUSE_PRESS_MIDDLE_BUTTON ||
                          evt == Mouse::MOUSE_PRESS_RIGHT_BUTTON ||
                          evt == Mouse::MOUSE_WHEEL) &&
                         point.x >= bounds.x &&
                         point.x <= bounds.x + bounds.width &&
                         point.y >= bounds.y &&
                         point.y <= bounds.y + bounds.height))
                    {
                        eventConsumed |= form->mouseEvent(evt, point.x - bounds.x, bounds.height - point.y - bounds.y, wheelDelta);
                    }
                }
            }
            else
            {
                // Simply compare with the form's bounds.
                const Rectangle& bounds = form->getBounds();
                if (form->getState() == Control::FOCUS ||
                    ((evt == Mouse::MOUSE_PRESS_LEFT_BUTTON ||
                      evt == Mouse::MOUSE_PRESS_MIDDLE_BUTTON ||
                      evt == Mouse::MOUSE_PRESS_RIGHT_BUTTON ||
                      evt == Mouse::MOUSE_WHEEL) &&
                        x >= bounds.x &&
                        x <= bounds.x + bounds.width &&
                        y >= bounds.y &&
                        y <= bounds.y + bounds.height))
                {
                    // Pass on the event's position relative to the form.
                    eventConsumed |= form->mouseEvent(evt, x - bounds.x, y - bounds.y, wheelDelta);
                }
            }
        }
    }
    return eventConsumed;
}

bool Form::projectPoint(int x, int y, Vector3* point)
{
    Scene* scene = _node->getScene();
    GP_ASSERT(scene);
    Camera* camera = scene->getActiveCamera();

    if (camera)
    {
        // Get info about the form's position.
        Matrix m = _node->getMatrix();
        Vector3 min(0, 0, 0);
        m.transformPoint(&min);

        // Unproject point into world space.
        Ray ray;
        camera->pickRay(Game::getInstance()->getViewport(), x, y, &ray);

        // Find the quad's plane.  We know its normal is the quad's forward vector.
        Vector3 normal = _node->getForwardVectorWorld();

        // To get the plane's distance from the origin, we'll find the distance from the plane defined
        // by the quad's forward vector and one of its points to the plane defined by the same vector and the origin.
        const float& a = normal.x; const float& b = normal.y; const float& c = normal.z;
        const float d = -(a*min.x) - (b*min.y) - (c*min.z);
        const float distance = abs(d) /  sqrt(a*a + b*b + c*c);
        Plane plane(normal, -distance);

        // Check for collision with plane.
        float collisionDistance = ray.intersects(plane);
        if (collisionDistance != Ray::INTERSECTS_NONE)
        {
            // Multiply the ray's direction vector by collision distance and add that to the ray's origin.
            point->set(ray.getOrigin() + collisionDistance*ray.getDirection());

            // Project this point into the plane.
            m.invert();
            m.transformPoint(point);

            return true;
        }
    }
    return false;
}

unsigned int Form::nextPowerOfTwo(unsigned int v)
{
    if (!((v & (v - 1)) == 0))
    {
        v--;
        v |= v >> 1;
        v |= v >> 2;
        v |= v >> 4;
        v |= v >> 8;
        v |= v >> 16;
        return v + 1;
    }
    else
    {
        return v;
    }
}

}
