//
// Copyright (c) 2008-2014 the Urho3D project.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

#pragma once

#include "../Math/Rect.h"
#include "../Math/Vector3.h"

namespace Atomic
{

class Polyhedron;
class Frustum;
class Matrix3;
class Matrix4;
class Matrix3x4;
class Sphere;

/// Three-dimensional axis-aligned bounding box.
class ATOMIC_API BoundingBox
{
public:
    /// Construct with zero size.
    BoundingBox() :
        min_(Vector3::ZERO),
        max_(Vector3::ZERO),
        defined_(false)
    {
    }
    
    /// Copy-construct from another bounding box.
    BoundingBox(const BoundingBox& box) :
        min_(box.min_),
        max_(box.max_),
        defined_(box.defined_)
    {
    }
    
    /// Construct from a rect, with the Z dimension left zero.
    BoundingBox(const Rect& rect) :
        min_(Vector3(rect.min_, 0.0f)),
        max_(Vector3(rect.max_, 0.0f)),
        defined_(true)
    {
    }
    
    /// Construct from minimum and maximum vectors.
    BoundingBox(const Vector3& min, const Vector3& max) :
        min_(min),
        max_(max),
        defined_(true)
    {
    }
    
    /// Construct from minimum and maximum floats (all dimensions same.)
    BoundingBox(float min, float max) :
        min_(Vector3(min, min, min)),
        max_(Vector3(max, max, max)),
        defined_(true)
    {
    }

    /// Construct from minimum and maximum floats (all dimensions defined.)
    BoundingBox(float minx, float miny, float minz, float maxx, float maxy, float maxz) :
        min_(Vector3(minx, miny, minz)),
        max_(Vector3(maxx, maxy, maxz)),
        defined_(true)
    {
    }

    BoundingBox(float* data) :
        min_(Vector3(data[0], data[1], data[2])),
        max_(Vector3(data[3], data[4], data[5])),
        defined_(true)
    {
    }


    
    /// Construct from an array of vertices.
    BoundingBox(const Vector3* vertices, unsigned count) :
        defined_(false)
    {
        Define(vertices, count);
    }
    
    /// Construct from a frustum.
    BoundingBox(const Frustum& frustum) :
        defined_(false)
    {
        Define(frustum);
    }
    
    /// Construct from a polyhedron.
    BoundingBox(const Polyhedron& poly) :
        defined_(false)
    {
        Define(poly);
    }
    
    /// Construct from a sphere.
    BoundingBox(const Sphere& sphere) :
        defined_(false)
    {
        Define(sphere);
    }
    
    /// Assign from another bounding box.
    BoundingBox& operator = (const BoundingBox& rhs)
    {
        min_ = rhs.min_;
        max_ = rhs.max_;
        defined_ = rhs.defined_;
        return *this;
    }
    
    /// Assign from a Rect, with the Z dimension left zero.
    BoundingBox& operator = (const Rect& rhs)
    {
        min_ = Vector3(rhs.min_, 0.0f);
        max_ = Vector3(rhs.max_, 0.0f);
        defined_ = true;
        return *this;
    }
    
    /// Test for equality with another bounding box.
    bool operator == (const BoundingBox& rhs) const { return (min_ == rhs.min_ && max_ == rhs.max_); }
    
    /// Test for inequality with another bounding box.
    bool operator != (const BoundingBox& rhs) const { return (min_ != rhs.min_ || max_ != rhs.max_); }
    
    /// Define from another bounding box.
    void Define(const BoundingBox& box)
    {
        Define(box.min_, box.max_);
    }
    
    /// Define from a Rect.
    void Define(const Rect& rect)
    {
        Define(Vector3(rect.min_, 0.0f), Vector3(rect.max_, 0.0f));
    }
    
    /// Define from minimum and maximum vectors.
    void Define(const Vector3& min, const Vector3& max)
    {
        min_ = min;
        max_ = max;
        defined_ = true;
    }
    
    /// Define from minimum and maximum floats (all dimensions same.)
    void Define(float min, float max)
    {
        min_ = Vector3(min, min, min);
        max_ = Vector3(max, max, max);
        defined_ = true;
    }
    
    /// Define from a point.
    void Define(const Vector3& point)
    {
        min_ = max_ = point;
        defined_ = true;
    }
    
    /// Merge a point.
    void Merge(const Vector3& point)
    {
        if (!defined_)
        {
            min_ = max_ = point;
            defined_ = true;
            return;
        }
        
        if (point.x_ < min_.x_)
            min_.x_ = point.x_;
        if (point.y_ < min_.y_)
            min_.y_ = point.y_;
        if (point.z_ < min_.z_)
            min_.z_ = point.z_;
        if (point.x_ > max_.x_)
            max_.x_ = point.x_;
        if (point.y_ > max_.y_)
            max_.y_ = point.y_;
        if (point.z_ > max_.z_)
            max_.z_ = point.z_;
    }
    
    /// Merge another bounding box.
    void Merge(const BoundingBox& box)
    {
        if (!defined_)
        {
            min_ = box.min_;
            max_ = box.max_;
            defined_ = true;
            return;
        }
    
        if (box.min_.x_ < min_.x_)
            min_.x_ = box.min_.x_;
        if (box.min_.y_ < min_.y_)
            min_.y_ = box.min_.y_;
        if (box.min_.z_ < min_.z_)
            min_.z_ = box.min_.z_;
        if (box.max_.x_ > max_.x_)
            max_.x_ = box.max_.x_;
        if (box.max_.y_ > max_.y_)
            max_.y_ = box.max_.y_;
        if (box.max_.z_ > max_.z_)
            max_.z_ = box.max_.z_;
    }
    
    /// Define from an array of vertices.
    void Define(const Vector3* vertices, unsigned count);
    /// Define from a frustum.
    void Define(const Frustum& frustum);
    /// Define from a polyhedron.
    void Define(const Polyhedron& poly);
    /// Define from a sphere.
    void Define(const Sphere& sphere);
    /// Merge an array of vertices.
    void Merge(const Vector3* vertices, unsigned count);
    /// Merge a frustum.
    void Merge(const Frustum& frustum);
    /// Merge a polyhedron.
    void Merge(const Polyhedron& poly);
    /// Merge a sphere.
    void Merge(const Sphere& sphere);
    /// Clip with another bounding box.
    void Clip(const BoundingBox& box);
    /// Transform with a 3x3 matrix.
    void Transform(const Matrix3& transform);
    /// Transform with a 3x4 matrix.
    void Transform(const Matrix3x4& transform);
    
    /// Clear to undefined state.
    void Clear()
    {
        min_ = Vector3::ZERO;
        max_ = Vector3::ZERO;
        defined_ = false;
    }
    
    /// Return center.
    Vector3 Center() const { return (max_ + min_) * 0.5f; }
    /// Return size.
    Vector3 Size() const { return max_ - min_; }
    /// Return half-size.
    Vector3 HalfSize() const { return (max_ - min_) * 0.5f; }
    
    /// Return transformed by a 3x3 matrix.
    BoundingBox Transformed(const Matrix3& transform) const;
    /// Return transformed by a 3x4 matrix.
    BoundingBox Transformed(const Matrix3x4& transform) const;
    /// Return projected by a 4x4 projection matrix.
    Rect Projected(const Matrix4& projection) const;
    
    /// Test if a point is inside.
    Intersection IsInside(const Vector3& point) const
    {
        if (point.x_ < min_.x_ || point.x_ > max_.x_ || point.y_ < min_.y_ || point.y_ > max_.y_ ||
            point.z_ < min_.z_ || point.z_ > max_.z_)
            return OUTSIDE;
        else
            return INSIDE;
    }
    
    /// Test if another bounding box is inside, outside or intersects.
    Intersection IsInside(const BoundingBox& box) const
    {
        if (box.max_.x_ < min_.x_ || box.min_.x_ > max_.x_ || box.max_.y_ < min_.y_ || box.min_.y_ > max_.y_ ||
            box.max_.z_ < min_.z_ || box.min_.z_ > max_.z_)
            return OUTSIDE;
        else if (box.min_.x_ < min_.x_ || box.max_.x_ > max_.x_ || box.min_.y_ < min_.y_ || box.max_.y_ > max_.y_ ||
            box.min_.z_ < min_.z_ || box.max_.z_ > max_.z_)
            return INTERSECTS;
        else
            return INSIDE;
    }
    
    /// Test if another bounding box is (partially) inside or outside.
    Intersection IsInsideFast(const BoundingBox& box) const
    {
        if (box.max_.x_ < min_.x_ || box.min_.x_ > max_.x_ || box.max_.y_ < min_.y_ || box.min_.y_ > max_.y_ ||
            box.max_.z_ < min_.z_ || box.min_.z_ > max_.z_)
            return OUTSIDE;
        else
            return INSIDE;
    }
    
    /// Test if a sphere is inside, outside or intersects.
    Intersection IsInside(const Sphere& sphere) const;
    /// Test if a sphere is (partially) inside or outside.
    Intersection IsInsideFast(const Sphere& sphere) const;
    
    /// Return as string.
    String ToString() const;

    float* Data() const
    {
        float* data = const_cast<BoundingBox*>(this)->data_;

        data[0] = min_.x_;
        data[1] = min_.y_;
        data[2] = min_.z_;
        data[3] = max_.x_;
        data[4] = max_.y_;
        data[5] = max_.z_;
        return data;
    }
    
    /// Minimum vector.
    Vector3 min_;
    /// Maximum vector.
    Vector3 max_;
    /// Defined flag.
    bool defined_;

    float data_[6];
};

}
