/*
 * Copyright (C) 2015 by Stefan Rothe
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY); without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package ch.jeda.physics;

import ch.jeda.Log;
import ch.jeda.MathF;
import ch.jeda.geometry.Shape;
import ch.jeda.ui.Alignment;
import ch.jeda.ui.Canvas;
import ch.jeda.ui.Color;
import ch.jeda.ui.Element;
import ch.jeda.ui.Image;
import java.util.HashSet;
import java.util.Set;

/**
 * Represents a body.
 *
 * @since 2.0
 */
public class Body extends Element {

    private final Set<Joint> joints;
    private final Set<Sensor> sensors;
    private Color debugColor;
    private boolean gravityIgnored;
    private float height;
    private Image image;
    private BodyImp imp;
    private boolean jointsDirty;
    private int opacity;
    private float width;

    /**
     * Creates a new object of a subclass of {@link ch.jeda.physics.Body}. If <code>className</code> does not meet all
     * of the following criteria, a new {@link ch.jeda.physics.Body} object is created and returned.
     * <ul>
     * <li><code>className</code> must be a valid class name
     * <li>The class must be a subclass of {@link ch.jeda.physics.Body}
     * <li>The class must provide a public default constructor
     * </ul>
     *
     * @param className the name of the class
     * @return the created object
     *
     * @since 2.0
     */
    public static Body create(final String className) {
        if (className == null || className.isEmpty()) {
            return new Body();
        }

        try {
            final Class clazz = Class.forName(className);
            if (Body.class.isAssignableFrom(clazz)) {
                return (Body) clazz.newInstance();
            }
        }
        catch (final ClassNotFoundException ex) {
        }
        catch (InstantiationException ex) {
        }
        catch (IllegalAccessException ex) {
        }

        return new Body();
    }

    /**
     * Constructs a body.
     *
     * @since 2.0
     */
    public Body() {
        joints = new HashSet<Joint>();
        sensors = new HashSet<Sensor>();
        image = null;
        imp = new DetachedBodyImp();
        jointsDirty = false;
        opacity = 255;
    }

    /**
     * Adds a shape to this body. Has no effect if <code>shape</code> is <code>null</code>.
     *
     * @param shape the shape to add
     *
     * @since 2.0
     */
    public final void addShape(final Shape shape) {
        if (shape != null) {
            imp.addShape(shape);
        }
    }

    /**
     * @deprecated Use {@link #applyLocalForceRad(double, double)} instead.
     *
     * @since 2.0
     */
    public final void applyForce(final double f) {
        applyLocalForceRad(f, 0.0);
    }

    /**
     * Applies a force to this body's center of mass. The unit of the force is Newton.
     *
     * @param fx the horizontal component of the force in Newton
     * @param fy the vertical component of the force in Newton
     *
     * @since 2.0
     */
    public final void applyForce(final double fx, final double fy) {
        imp.applyForce((float) fx, (float) fy);
    }

    /**
     * Applies a force to this body at the specified point. The unit of the force is Newton. The force is applied at the
     * point (<code>x</code>, <code>y</code>) in world coordinates.
     *
     * @param fx the horizontal component of the force in Newton
     * @param fy the vertical component of the force in Newton
     * @param x the horizontal world coordinate of the point of origin of the force
     * @param y the vertical world coordinate of the point of origin of the force
     *
     * @since 2.0
     */
    public final void applyForce(final double fx, final double fy, final double x, final double y) {
        imp.applyForce((float) fx, (float) fy, (float) x, (float) y);
    }

    /**
     * Applies a force to this body's center of mass in a direction relative to the body's angle. The unit of the force
     * is Newton. Use this method to simulate a force generated by the body itself, e.g. a propulsion system.
     *
     * @param force the force in Newton
     * @param angle the angle relative to the body's angle in degrees
     *
     * @since 2.1
     */
    protected final void applyLocalForceDeg(final double force, final double angle) {
        applyLocalForceRad(force, Math.toRadians(angle));
    }

    /**
     * Applies a force to this body's center of mass in a direction relative to the body's angle. The unit of the force
     * is Newton. Use this method to simulate a force generated by the body itself, e.g. a propulsion system.
     *
     * @param force the force in Newton
     * @param angle the angle relative to the body's angle in radians
     *
     * @since 2.1
     */
    protected final void applyLocalForceRad(final double force, final double angle) {
        imp.applyLocalForceRad((float) force, (float) angle);
    }

    /**
     * Applies a force to this body a the specified point in a direction relative to the body's angle. The unit of the
     * force is Newton. The force is applied at the point (<code>x</code>, <code>y</code>) in local coordinates relative
     * to the position and angle of the body (but <b>not</b> relative to the body's center of mass). Use this method to
     * simulate a force generated by the body itself, e.g. a propulsion system.
     *
     * @param force the force in Newton
     * @param angle the angle relative to the body's angle in degrees
     * @param x the horizontal local coordinate of the point of origin of the force
     * @param y the vertical local coordinate of the point of origin of the force
     *
     * @since 2.1
     */
    protected final void applyLocalForceDeg(final double force, final double angle, final double x, final double y) {
        applyLocalForceRad(force, Math.toRadians(angle), x, y);
    }

    /**
     * Applies a force to this body a the specified point in a direction relative to the body's angle. The unit of the
     * force is Newton. The force is applied at the point (<code>x</code>, <code>y</code>) in local coordinates relative
     * to the position and angle of the body (but <b>not</b> relative to the body's center of mass). Use this method to
     * simulate a force generated by the body itself, e.g. a propulsion system.
     *
     * @param force the force in Newton
     * @param angle the angle relative to the body's angle in radians
     * @param x the horizontal local coordinate of the point of origin of the force
     * @param y the vertical local coordinate of the point of origin of the force
     *
     * @since 2.1
     */
    protected final void applyLocalForceRad(final double force, final double angle, final double x, final double y) {
        imp.applyLocalForceRad((float) force, (float) angle, (float) x, (float) y);
    }

    /**
     * Applies a torque to this body. The unit of the torque is Newton meter.
     *
     * @param torque the torque in Newton meter
     *
     * @since 2.0
     */
    public final void applyTorque(final double torque) {
        imp.applyTorque((float) torque);
    }

    /**
     * Returns the current angle of this body in radians.
     *
     * @return the current angle of this body in radians
     *
     * @see #getAngleDeg()
     * @see #setAngleDeg(double)
     * @see #setAngleRad(double)
     * @since 2.0
     */
    @Override
    public final float getAngleRad() {
        return imp.getAngleRad();
    }

    /**
     * Returns the angular damping of this body.
     *
     * @return the angular damping of this body
     *
     * @see #setAngularDamping(double)
     * @since 2.0
     */
    public final float getAngularDamping() {
        return imp.getAngularDamping();
    }

    /**
     * Returns the current angular velocity of this body in radians per second.
     *
     * @return the current angular velocity of this body in radians per second
     *
     * @see #setAngularVelocity(double)
     * @since 2.0
     */
    public final float getAngularVelocity() {
        return imp.getAngularVelocity();
    }

    /**
     * Returns the linear damping of this body.
     *
     * @return the linear damping of this body
     *
     * @see #setDamping(double)
     * @since 2.0
     */
    public final float getDamping() {
        return imp.getDamping();
    }

    /**
     * Returns the debug color of this body. The debug color is used for drawing the debug overlay of this body.
     *
     * @return the debug color of this body
     *
     * @since 2.0
     */
    public Color getDebugColor() {
        return debugColor;
    }

    /**
     * Returns the density of this body. The unit of the density is kilograms per square meter
     *
     * @return the density of this body in kilograms per square meter
     *
     * @see #setDensity(double)
     * @since 2.0
     */
    public final float getDensity() {
        return imp.getDensity();
    }

    /**
     * Returns the current direction of this body in degrees. The direction is derived from the body's velocity and
     * indicates in which direction the body is moving. It is independent of the body's orientation indicated by
     * {@link #getAngleDeg()}.
     *
     * @return the current direction of this body in degrees
     *
     * @see #getDirectionRad()
     * @since 2.2
     */
    public final float getDirectionDeg() {
        return MathF.toDegrees(imp.getDirectionRad());
    }

    /**
     * Returns the current direction of this body in radians. The direction is derived from the body's velocity and
     * indicates in which direction the body is moving. It is independent of the body's orientation indicated by
     * {@link #getAngleDeg()}.
     *
     * @return the current direction of this body in radians
     *
     * @see #getDirectionDeg()
     * @since 2.2
     */
    public final float getDirectionRad() {
        return imp.getDirectionRad();
    }

    /**
     * Returns the friction of this body.
     *
     * @return the friction of this body
     *
     * @see #setFriction(double)
     * @since 2.0
     */
    public final float getFriction() {
        return imp.getFriction();
    }

    /**
     * Returns the height of this body in meters. The body height determines the size of the image representing the body
     * visually. It has no impact on the physical behavior of the body.
     *
     * @return the height of this body in meters
     *
     * @since 2.0
     */
    public final float getHeight() {
        return height;
    }

    /**
     * Returns the image of this body.
     *
     * @return the image of this body
     *
     * @see #setImage(ch.jeda.ui.Image, double, double)
     * @since 2.0
     */
    public final Image getImage() {
        return image;
    }

    /**
     * Returns the mass of this body in kilograms. The body's mass is determined by it's density and the areas of it's
     * shapes.
     *
     * @return the mass of this body in kilograms
     *
     * @see #addShape(ch.jeda.geometry.Shape)
     * @see #getDensity()
     * @see #getShapes()
     * @see #setDensity(double)
     * @since 2.0
     */
    public final float getMass() {
        return imp.getMass();
    }

    /**
     * Returns the opacity of this body. The opacity is a number between 0 and 255 where 0 means totally transparent and
     * 255 means totally opaque. The opacity has no impact on the physical behavior of the body.
     *
     * @return the opacity of this body
     *
     * @see #setOpacity(int)
     * @since 2.0
     */
    public final int getOpacity() {
        return opacity;
    }

    public Sensor[] getSensors() {
        return sensors.toArray(new Sensor[sensors.size()]);
    }

    /**
     * Returns the shapes of this body.
     *
     * @return the shapes of this body
     *
     * @since 2.0
     */
    public final Shape[] getShapes() {
        return imp.getShapes();
    }

    /**
     * Returns the type of this body.
     *
     * @return the type of this body
     *
     * @see #setType(ch.jeda.physics.BodyType)
     * @since 2.0
     */
    public final BodyType getType() {
        return imp.getType();
    }

    /**
     * Returns this body's current linear velocity in meters per second.
     *
     * @return this body's current linear velocity in meters per second
     *
     * @see #setVelocity(double, double)
     * @see #getVx()
     * @see #getVy()
     * @since 2.1
     */
    public final float getVelocity() {
        return imp.getVelocity();
    }

    /**
     * Returns the horizontal component of this body's current linear velocity in meters per second.
     *
     * @return the horizontal component of this body's current linear velocity in meters per second
     *
     * @see #setVelocity(double, double)
     * @see #getVelocity()
     * @see #getVy()
     * @since 2.0
     */
    public final float getVx() {
        return imp.getVx();
    }

    /**
     * Returns the vertical component of this body's current linear velocity in meters per second.
     *
     * @return the vertical component of this body's current linear velocity in meters per second
     *
     * @see #setVelocity(double, double)
     * @see #getVelocity()
     * @see #getVx()
     * @since 2.0
     */
    public final float getVy() {
        return imp.getVy();
    }

    /**
     * Returns the width of this body in meters. The body width determines the size of the image representing the body
     * visually. It has no impact on the physical behavior of the body.
     *
     * @return the width of this body in meters
     *
     * @since 2.0
     */
    public final float getWidth() {
        return width;
    }

    /**
     * Returns the x coordinate of this body in pixels.
     *
     * @return the x coordinate of the body in pixels
     *
     * @see #setPosition(double, double)
     * @see #getY()
     * @since 2.0
     */
    @Override
    public final float getX() {
        return imp.getX();
    }

    /**
     * Returns the y coordinate of this body in pixels.
     *
     * @return the y coordinate of this body in pixels
     *
     * @see #setPosition(double, double)
     * @see #getX()
     * @since 2.0
     */
    @Override
    public final float getY() {
        return imp.getY();
    }

    /**
     * Checks if this body ignores gravity.
     *
     * @return <code>true</code> if this body ignores gravity, otherwise <code>false</code>
     *
     * @see #setGravityIgnored(boolean)
     * @since 2.5
     */
    public boolean isGravityIgnored() {
        return gravityIgnored;
    }

    /**
     * Checks if this body is able to rotate.
     *
     * @return <code>true</code> if this body is <b>not</b> able to rotate, otherwise <code>false</code>
     *
     * @see #setRotationFixed(boolean)
     * @since 2.5
     */
    public final boolean isRotationFixed() {
        return imp.isRotationFixed();
    }

    /**
     * Sets the rotation angle of this body in radians. If the body is in a physics simulation, the body will be removed
     * from the simulation, then re-added to the simulation with the new angle.
     *
     * @param angle the angle of this body in radians
     *
     * @see #getAngleDeg()
     * @see #getAngleRad()
     * @see #setAngleDeg(double)
     * @since 2.0
     */
    @Override
    public final void setAngleRad(final double angle) {
        final Physics physics = imp.getPhysics();
        if (physics == null) {
            imp.setAngleRad((float) angle);
        }
        else {
            BodyImp detachedImp = new DetachedBodyImp(imp);
            detachedImp.setAngleRad((float) angle);
            imp.destroy();
            imp = new PhysicsBodyImp(physics, this, detachedImp);
        }
    }

    /**
     * Sets the angular damping of this body.
     *
     * @param angularDamping the angular damping
     *
     * @see #getAngularDamping()
     * @since 2.0
     */
    public final void setAngularDamping(final double angularDamping) {
        imp.setAngularDamping((float) angularDamping);
    }

    /**
     * Sets the angular velocity of this body in radians per second.
     *
     * @param angularVelocity the angular velocity in radians per second
     *
     * @see #getAngularVelocity()
     * @since 2.0
     */
    public final void setAngularVelocity(final double angularVelocity) {
        imp.setAngularVelocity((float) angularVelocity);
    }

    /**
     * Sets the linear damping of this body.
     *
     * @param damping the linear damping
     *
     * @see #getDamping()
     * @since 2.0
     */
    public final void setDamping(final double damping) {
        imp.setDamping((float) damping);
    }

    /**
     * Sets the debug color of this body. The debug color is used for drawing the debug overlay of this body.
     *
     * @param debugColor the debug color of this body.
     *
     * @since 2.0
     */
    public void setDebugColor(final Color debugColor) {
        this.debugColor = debugColor;
    }

    /**
     * Sets the density of this body in kilogram per square meter.
     *
     * @param density the density of this body in kilogram per square meter
     *
     * @see #getDensity()
     * @since 2.0
     */
    public final void setDensity(final double density) {
        imp.setDensity((float) density);
    }

    /**
     * Sets the friction of this body.
     *
     * @param friction the friction of this body
     *
     * @see #getFriction()
     * @since 2.0
     */
    public final void setFriction(final double friction) {
        imp.setFriction((float) friction);
    }

    /**
     * Disables or enables the effect of gravity on this body.
     *
     * @param gravityIgnored <code>true</code> to disable gravity for this body, <code>false</code> to enable it
     *
     * @see #isGravityIgnored()
     * @since 2.5
     */
    public void setGravityIgnored(final boolean gravityIgnored) {
        this.gravityIgnored = gravityIgnored;
        imp.setGravityIgnored(gravityIgnored);
    }

    /**
     * Sets the image for this body. The opacity has no impact on the physical behavior of the body.
     *
     * @param image the image
     * @param width the width of the image in meters
     * @param height the height of the image in meters
     *
     * @see #getImage()
     * @since 2.1
     */
    public final void setImage(final Image image, final double width, final double height) {
        setImage(image, (float) width, (float) height);
    }

    /**
     * Sets the image for this body. The opacity has no impact on the physical behavior of the body.
     *
     * @param image the image
     * @param width the width of the image in meters
     * @param height the height of the image in meters
     *
     * @see #getImage()
     * @since 2.0
     */
    public final void setImage(final Image image, final float width, final float height) {
        this.image = image;
        this.width = width;
        this.height = height;
    }

    /**
     * Sets the opacity of this body. The opacity is a number between 0 and 255 where 0 means totally transparent and
     * 255 means totally opaque. The opacity has no impact on the physical behavior of the body.
     *
     * @param opacity the opacity of this body
     *
     * @see #getOpacity()
     * @since 2.0
     */
    public final void setOpacity(final int opacity) {
        this.opacity = Math.max(0, Math.min(opacity, 255));
    }

    /**
     * Sets the position of this body. If the body is in a physics simulation, the body will be removed from the
     * simulation, then added to the simulation at the specified coordinates.
     *
     * @param x the x coordinate of this body in pixels
     * @param y the y coordinate of this body in pixels
     *
     * @see #getX()
     * @see #getY()
     * @since 2.0
     */
    @Override
    public final void setPosition(final double x, final double y) {
        final Physics physics = imp.getPhysics();
        if (physics == null) {
            imp.setPosition((float) x, (float) y);
        }
        else {
            BodyImp detachedImp = new DetachedBodyImp(imp);
            detachedImp.setPosition((float) x, (float) y);
            imp.destroy();
            imp = new PhysicsBodyImp(physics, this, detachedImp);
        }
    }

    /**
     * Disables or enables this body to rotate.
     *
     * @param fixed <code>true</code> to disable body rotation, <code>false</code> to enable it
     *
     * @since 2.0
     */
    public final void setRotationFixed(final boolean fixed) {
        imp.setRotationFixed(fixed);
    }

    /**
     * Sets the type of this body.
     *
     * @param type the type of this body
     *
     * @see #getType()
     * @since 2.0
     */
    public final void setType(final BodyType type) {
        if (type == null) {
            Log.d("Ignoring call with null body type.");
        }
        else {
            imp.setType(type);
        }
    }

    /**
     * Sets the velocity of this body.
     *
     * @param vx the horizontal component of the velocity
     * @param vy the vertical component of the velocity
     *
     * @see #getVelocity()
     * @see #getVx()
     * @see #getVy()
     * @since 2.0
     */
    public final void setVelocity(final double vx, final double vy) {
        imp.setVelocity((float) vx, (float) vy);
    }

    /**
     * Invoked when this body just has made contact with another body. Override this method to add new behavior.
     *
     * @param other the other body
     *
     * @since 2.0
     */
    protected void beginContact(final Body other) {
    }

    @Override
    protected final void draw(final Canvas canvas) {
        canvas.setAlignment(Alignment.CENTER);
        canvas.setOpacity(opacity);
        if (image != null) {
            canvas.drawImage(0f, 0f, width, height, image);
        }

        drawDecoration(canvas);
        imp.drawOverlay(canvas);
    }

    /**
     * Invoked after the image of this body has been drawn. Override this method to add new behaviour.
     *
     * @param canvas the canvas to draw on
     *
     * @since 2.0
     */
    protected void drawDecoration(final Canvas canvas) {
    }

    /**
     * Invoked when the contact with another body ends. Override this method to add new behavior.
     *
     * @param other the other body
     *
     * @since 2.0
     */
    protected void endContact(final Body other) {
    }

    /**
     * Checks if this body should collide with the other body. By default, this method always returns <code>true</code>.
     * Override this method to prevent some collisions.
     *
     * @param other the other body.
     * @return <code>true</code> if a collision check should be performed for the two bodies, otherwise
     * <code>false</code>
     *
     * @since 2.1
     */
    protected boolean shouldCollide(final Body other) {
        return true;
    }

    final void addJoint(final Joint joint) {
        joints.add(joint);
        jointsDirty = true;
    }

    final void addSensor(final Sensor sensor) {
        sensors.add(sensor);
        imp.addSensor(sensor);
    }

    void checkJoints() {
        if (!jointsDirty) {
            return;
        }

        for (final Joint joint : joints) {
            final Body otherBody = joint.getOtherBody(this);
            final Physics physics = imp.getPhysics();
            if (physics == otherBody.imp.getPhysics()) {
                joint.setPhysics(physics);
            }
            else {
                joint.setPhysics(null);
            }
        }

        jointsDirty = false;
    }

    final org.jbox2d.dynamics.Body getJBoxBody() {
        return imp.getJBoxBody();
    }

    final BodyImp getImp() {
        return imp;
    }

    final Physics getPhysics() {
        return imp.getPhysics();
    }

    final void internalBeginContact(final Body other) {
        if (imp.getPhysics() != null && getView() != null) {
            beginContact(other);
        }
    }

    final void internalEndContact(final Body other) {
        if (imp.getPhysics() != null && getView() != null) {
            endContact(other);
        }
    }

    final void removeJoint(final Joint joint) {
        joints.remove(joint);
    }

    final boolean setPhysics(final Physics physics) {
        if (imp.belongsTo(physics)) {
            return false;
        }

        final BodyImp oldImp = imp;
        if (physics == null) {
            imp = new DetachedBodyImp(oldImp);
            checkJoints();
        }
        else {
            imp = new PhysicsBodyImp(physics, this, oldImp);
            checkJoints();
        }

        oldImp.destroy();
        return true;
    }
}
