/**
 * 
 */
package org.openstreetmap.gui.jmapviewer.scene.entities;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.util.LinkedList;
import java.util.List;

import org.openstreetmap.gui.jmapviewer.Coordinate;
import org.openstreetmap.gui.jmapviewer.OsmMercator;
import org.openstreetmap.gui.jmapviewer.interfaces.MapViewInterface;

/**
 * Abstract Map Object for defining objects in OSM Space.
 *
 * @author Jason Huntley
 */
public abstract class MapObject {

    /** The child object list. */
    private final List<MapObject> childObjectList;

    /** The parent. */
    private MapObject parent = null;

    /** The local coordinate. */
    private Point localCoordinate=new Point();

    /** The world coordinate. */
    private Coordinate worldCoordinate=new Coordinate(0,0);

    /** The scale. */
    private final double scale = 1.0;

    /** The rotate. */
    private double rotate = 0;

    /** The at. */
    private AffineTransform at = new AffineTransform();

    /**
     * Instantiates a new map object.
     */
    public MapObject() {
        childObjectList = new LinkedList<MapObject>();
    }

    /**
     * Gets the at.
     *
     * @return the at
     */
    public AffineTransform getAt() {
        return at;
    }

    /**
     * Sets the at.
     *
     * @param at the at to set
     */
    public void setAt(AffineTransform at) {
        this.at = at;
    }

    /**
     * Adds the map object.
     *
     * @param obj the obj
     */
    public void addMapObject(MapObject obj) {
        obj.setParent(this);
        childObjectList.add(obj);
    }

    /**
     * Removes the map object.
     *
     * @param obj the obj
     */
    public void removeMapObject(MapObject obj) {
        childObjectList.remove(obj);
    }

    /**
     * Gets the parent.
     *
     * @return the parent
     */
    public MapObject getParent() {
        return parent;
    }

    /**
     * Sets the parent.
     *
     * @param parent the parent to set
     */
    public void setParent(MapObject parent) {
        this.parent = parent;
    }

    /**
     * Initialize state of object.
     *
     * @param view the view
     */
    public abstract void initialize(MapViewInterface view);

    /**
     * Gets the local scale.
     *
     * @return the local scale
     */
    public double getLocalScale() {
        return scale;
    }

    /**
     * Gets the local coordinates.
     *
     * @return the local coordinates
     */
    public Point getLocalCoordinates() {
        return localCoordinate;
    }

    /**
     * Sets the local screen coordinates.
     *
     * @param pt the new local coordinates
     */
    public void setLocalCoordinates(Point pt) {
        if (pt==null)
            localCoordinate=new Point(0,0);
        else
            localCoordinate=new Point(pt);
    }

    /**
     * Sets the local screen coordinates.
     *
     * @param x the x
     * @param y the y
     */
    public void setLocalCoordinates(int x, int y) {
        this.setLocalCoordinates(new Point(x,y));
    }

    /**
     * Gets the world coordinate.
     *
     * @return the worldCoordinate
     */
    public Coordinate getWorldCoordinate() {
        return worldCoordinate;
    }

    /**
     * Sets the world coordinate.
     *
     * @param worldCoordinate the worldCoordinate to set
     */
    public void setWorldCoordinate(Coordinate worldCoordinate) {
        this.worldCoordinate = worldCoordinate;
    }

    /**
     * Sets the world coordinate.
     *
     * @param lat the lat
     * @param lon the lon
     */
    public void setWorldCoordinate(double lat, double lon) {
        this.worldCoordinate = new Coordinate(lat,lon);
    }

    /**
     * Gets the rotate.
     *
     * @return the rotate in radians
     */
    public double getRotate() {
        return rotate;
    }

    /**
     * Sets the rotate.
     *
     * @param radians the new rotate
     */
    public void setRotate(double radians) {
        this.rotate = radians;
    }

    /**
     * Sets the rotate degrees.
     *
     * @param degrees the new rotate degrees
     */
    public void setRotateDegrees(double degrees) {
        this.rotate = OsmMercator.degToRad(degrees);
    }

    /**
     * Update local. If window resizes or zoom occurs, the local
     * coordinates will change on base level nodes.
     *
     * @param view the view
     */
    public void updateLocal(MapViewInterface view) {
        if (parent==null) {
            Point pt=view.getMapPosition(getWorldCoordinate(),false);
            this.setLocalCoordinates(pt);
        }
    }

    /**
     * Update world. After local transformations occur,
     * the global coordinates need to be update to reflect
     * new positions.
     *
     * @param view the view
     * @param nextAt the next at
     */
    public void updateWorld(MapViewInterface view, AffineTransform nextAt) {
        Point next=new Point((int)nextAt.getTranslateX(),(int)nextAt.getTranslateY());
        setWorldCoordinate(view.getPosition(next));
    }

    /**
     * Update.
     *
     * @param view the view
     */
    public void update(MapViewInterface view) {
        updateLocal(view);

        if (parent!=null) {
            getAt().setToIdentity();
            getAt().setTransform(parent.getAt());

            at.translate(getLocalCoordinates().x, getLocalCoordinates().y);

            at.scale(getLocalScale(), getLocalScale());

            at.rotate(getRotate());
        } else {
            double actualScale=getLocalScale()/view.getMeterPerPixel();

            at.setToIdentity();

            at.translate(getLocalCoordinates().x, getLocalCoordinates().y);

            at.scale(actualScale, actualScale);

            at.rotate(getRotate());
        }

        updateWorld(view, at);

        for (MapObject obj : childObjectList) {
            obj.update(view);
        }
    }

    /**
     * Draw object.
     *
     * @param g2 the g2
     * @param at the at
     */
    protected abstract void drawObject(Graphics2D g2, AffineTransform at);

    /**
     * Draw.
     *
     * @param g the g
     * @param view the view
     */
    public void draw(Graphics g, MapViewInterface view) {
        Graphics2D g2 = (Graphics2D) g;

        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        drawObject(g2, at);

        for (MapObject obj : childObjectList) {
            obj.draw(g2, view);
        }
    }
}
