/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package jme3tools.navigation;

import java.awt.Point;
import java.text.DecimalFormat;

/**
 * A representation of the actual map in terms of lat/long and x,y co-ordinates.
 * The Map class contains various helper methods such as methods for determining
 * the pixel positions for lat/long co-ordinates and vice versa.
 *
 * @author Cormac Gebruers
 * @author Benjamin Jakobus
 * @version 1.0
 * @since 1.0
 */
public class MapModel2D {

    /* The number of radians per degree */
    private final static double RADIANS_PER_DEGREE = 57.2957;

    /* The number of degrees per radian */
    private final static double DEGREES_PER_RADIAN = 0.0174532925;

    /* The map's width in longitude */
    public final static int DEFAULT_MAP_WIDTH_LONGITUDE = 360;

    /* The top right hand corner of the map */
    private Position centre;

    /* The x and y co-ordinates for the viewport's centre */
    private int xCentre;
    private int yCentre;

    /* The width (in pixels) of the viewport holding the map */
    private int viewportWidth;

    /* The viewport height in pixels */
    private int viewportHeight;

    /* The number of minutes that one pixel represents */
    private double minutesPerPixel;

    /**
     * Constructor
     * @param viewportWidth the pixel width of the viewport (component) in which
     *        the map is displayed
     * @since 1.0
     */
    public MapModel2D(int viewportWidth) {
        try {
            this.centre = new Position(0, 0);
        } catch (InvalidPositionException e) {
            e.printStackTrace();
        }

        this.viewportWidth = viewportWidth;

        // Calculate the number of minutes that one pixel represents along the longitude
        calculateMinutesPerPixel(DEFAULT_MAP_WIDTH_LONGITUDE);

        // Calculate the viewport height based on its width and the number of degrees (85)
        // in our map
        viewportHeight = ((int) NavCalculator.computeDMPClarkeSpheroid(0, 85) / (int) minutesPerPixel) * 2;
//        viewportHeight = viewportWidth; // REMOVE!!!
        // Determine the map's x,y centre
        xCentre = viewportWidth / 2;
        yCentre = viewportHeight / 2;
    }

    /**
     * Returns the height of the viewport in pixels
     * @return the height of the viewport in pixels
     * @since 0.1
     */
    public int getViewportPixelHeight() {
        return viewportHeight;
    }

    /**
     * Calculates the number of minutes per pixels using a given
     * map width in longitude
     * @param mapWidthInLongitude
     * @since 1.0
     */
    public void calculateMinutesPerPixel(double mapWidthInLongitude) {
        minutesPerPixel = (mapWidthInLongitude * 60) / (double) viewportWidth;
    }

    /**
     * Returns the width of the viewport in pixels
     * @return the width of the viewport in pixels
     * @since 0.1
     */
    public int getViewportPixelWidth() {
        return viewportWidth;
    }

    public void setViewportWidth(int viewportWidth) {
        this.viewportWidth = viewportWidth;
    }

    public void setViewportHeight(int viewportHeight) {
        this.viewportHeight = viewportHeight;
    }

    public void setCentre(Position centre) {
        this.centre = centre;
    }

    /**
     * Returns the number of minutes there are per pixel
     * @return the number of minutes per pixel
     * @since 1.0
     */
    public double getMinutesPerPixel() {
        return minutesPerPixel;
    }

    public double getMetersPerPixel() {
        return 1853 * minutesPerPixel;
    }

    public void setMinutesPerPixel(double minutesPerPixel) {
        this.minutesPerPixel = minutesPerPixel;
    }

    /**
     * Converts a latitude/longitude position into a pixel co-ordinate
     * @param position the position to convert
     * @return {@code Point} a pixel co-ordinate
     * @since 1.0
     */
    public Point toPixel(Position position) {
        // Get the distance between position and the centre for calculating
        // the position's longitude translation
        double distance = NavCalculator.computeLongDiff(centre.getLongitude(),
                position.getLongitude());

        // Use the distance from the centre to calculate the pixel x co-ordinate
        double distanceInPixels = (distance / minutesPerPixel);

        // Use the difference in meridional parts to calculate the pixel y co-ordinate
        double dmp = NavCalculator.computeDMPClarkeSpheroid(centre.getLatitude(),
                position.getLatitude());

        int x = 0;
        int y = 0;

        if (centre.getLatitude() == position.getLatitude()) {
            y = yCentre;
        }
        if (centre.getLongitude() == position.getLongitude()) {
            x = xCentre;
        }

        // Distinguish between northern and southern hemisphere for latitude calculations
        if (centre.getLatitude() > 0 && position.getLatitude() > centre.getLatitude()) {
            // Centre is north. Position is north of centre
            y = yCentre + (int) ((dmp) / minutesPerPixel);
        } else if (centre.getLatitude() > 0 && position.getLatitude() < centre.getLatitude()) {
            // Centre is north. Position is south of centre
            y = yCentre - (int) ((dmp) / minutesPerPixel);
        } else if (centre.getLatitude() < 0 && position.getLatitude() > centre.getLatitude()) {
            // Centre is south. Position is north of centre
            y = yCentre + (int) ((dmp) / minutesPerPixel);
        } else if (centre.getLatitude() < 0 && position.getLatitude() < centre.getLatitude()) {
            // Centre is south. Position is south of centre
            y = yCentre - (int) ((dmp) / minutesPerPixel);
        } else if (centre.getLatitude() == 0 && position.getLatitude() > centre.getLatitude()) {
            // Centre is at the equator. Position is north of the equator
            y = yCentre + (int) ((dmp) / minutesPerPixel);
        } else if (centre.getLatitude() == 0 && position.getLatitude() < centre.getLatitude()) {
            // Centre is at the equator. Position is south of the equator
            y = yCentre - (int) ((dmp) / minutesPerPixel);
        }

        // Distinguish between western and eastern hemisphere for longitude calculations
        if (centre.getLongitude() < 0 && position.getLongitude() < centre.getLongitude()) {
            // Centre is west. Position is west of centre
            x = xCentre - (int) distanceInPixels;
        } else if (centre.getLongitude() < 0 && position.getLongitude() > centre.getLongitude()) {
            // Centre is west. Position is south of centre
            x = xCentre + (int) distanceInPixels;
        } else if (centre.getLongitude() > 0 && position.getLongitude() < centre.getLongitude()) {
            // Centre is east. Position is west of centre
            x = xCentre - (int) distanceInPixels;
        } else if (centre.getLongitude() > 0 && position.getLongitude() > centre.getLongitude()) {
            // Centre is east. Position is east of centre
            x = xCentre + (int) distanceInPixels;
        } else if (centre.getLongitude() == 0 && position.getLongitude() > centre.getLongitude()) {
            // Centre is at the equator. Position is east of centre
            x = xCentre + (int) distanceInPixels;
        } else if (centre.getLongitude() == 0 && position.getLongitude() < centre.getLongitude()) {
            // Centre is at the equator. Position is west of centre
            x = xCentre - (int) distanceInPixels;
        }

        // Distinguish between northern and souterhn hemisphere for longitude calculations
        return new Point(x, y);
    }

    /**
     * Converts a pixel position into a mercator position
     * @param p {@link Point} object that you wish to convert into
     *        longitude / latiude
     * @return the converted {@code Position} object
     * @since 1.0
     */
    public Position toPosition(Point p) {
        double lat, lon;
        Position pos = null;
        try {
            Point pixelCentre = toPixel(new Position(0, 0));

            // Get the distance between position and the centre
            double xDistance = distance(xCentre, p.getX());
            double yDistance = distance(pixelCentre.getY(), p.getY());
            double lonDistanceInDegrees = (xDistance * minutesPerPixel) / 60;
            double mp = (yDistance * minutesPerPixel);
            // If we are zoomed in past a certain point, then use linear search.
            // Otherwise use binary search
            if (getMinutesPerPixel() < 0.05) {
                lat = findLat(mp, getCentre().getLatitude());
                if (lat == -1000) {
                    System.out.println("lat: " + lat);
                }
            } else {
                lat = findLat(mp, 0.0, 85.0);
            }
            lon = (p.getX() < xCentre ? centre.getLongitude() - lonDistanceInDegrees
                    : centre.getLongitude() + lonDistanceInDegrees);

            if (p.getY() > pixelCentre.getY()) {
                lat = -1 * lat;
            }
            if (lat == -1000 || lon == -1000) {
                return pos;
            }
            pos = new Position(lat, lon);
        } catch (InvalidPositionException ipe) {
            ipe.printStackTrace();
        }
        return pos;
    }

    /**
     * Calculates distance between two points on the map in pixels
     * @param a
     * @param b
     * @return distance the distance between a and b in pixels
     * @since 1.0
     */
    private double distance(double a, double b) {
        return Math.abs(a - b);
    }

    /**
     * Defines the centre of the map in pixels
     * @param p <code>Point</code> object denoting the map's new centre
     * @since 1.0
     */
    public void setCentre(Point p) {
        try {
            Position newCentre = toPosition(p);
            if (newCentre != null) {
                centre = newCentre;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Sets the map's xCentre
     * @param xCentre
     * @since 1.0
     */
    public void setXCentre(int xCentre) {
        this.xCentre = xCentre;
    }

    /**
     * Sets the map's yCentre
     * @param yCentre
     * @since 1.0
     */
    public void setYCentre(int yCentre) {
        this.yCentre = yCentre;
    }

    /**
     * Returns the pixel (x,y) centre of the map
     * @return {@link Point} object marking the map's (x,y) centre
     * @since 1.0
     */
    public Point getPixelCentre() {
        return new Point(xCentre, yCentre);
    }

    /**
     * Returns the {@code Position} centre of the map
     * @return {@code Position} object marking the map's (lat, long) centre
     * @since 1.0
     */
    public Position getCentre() {
        return centre;
    }

    /**
     * Uses binary search to find the latitude of a given MP.
     *
     * @param mp maridian part
     * @param low
     * @param high
     * @return the latitude of the MP value
     * @since 1.0
     */
    private double findLat(double mp, double low, double high) {
        DecimalFormat form = new DecimalFormat("#.####");
        mp = Math.round(mp);
        double midLat = (low + high) / 2.0;
        // ctr is used to make sure that with some
        // numbers which can't be represented exactly don't inifitely repeat
        double guessMP = NavCalculator.computeDMPClarkeSpheroid(0, (float) midLat);

        while (low <= high) {
            if (guessMP == mp) {
                return midLat;
            } else {
                if (guessMP > mp) {
                    high = midLat - 0.0001;
                } else {
                    low = midLat + 0.0001;
                }
            }

            midLat = Double.valueOf(form.format(((low + high) / 2.0)));
            guessMP = NavCalculator.computeDMPClarkeSpheroid(0, (float) midLat);
            guessMP = Math.round(guessMP);
        }
        return -1000;
    }

    /**
     * Uses linear search to find the latitude of a given MP
     * @param mp the meridian part for which to find the latitude
     * @param previousLat the previous latitude. Used as a upper / lower bound
     * @return the latitude of the MP value
     */
    private double findLat(double mp, double previousLat) {
        DecimalFormat form = new DecimalFormat("#.#####");
        mp = Double.parseDouble(form.format(mp));
        double guessMP;
        for (double lat = previousLat - 0.25; lat < previousLat + 1; lat += 0.00001) {
            guessMP = NavCalculator.computeDMPClarkeSpheroid(0, lat);
            guessMP = Double.parseDouble(form.format(guessMP));
            if (guessMP == mp || Math.abs(guessMP - mp) < 0.001) {
                return lat;
            }
        }
        return -1000;
    }
}
