'use strict';

(function() {

    // fixme: make all this work with different types of "points"
    // i.e. {lat, lng}, [lat, lng]

    function deg(point) {
        return Ox.map(point, function(val) {
            return Ox.mod(Ox.deg(val) + 180, 360) - 180;
        });
    }

    function rad(point) {
        return Ox.map(point, function(val) {
            return Ox.rad(val);
        });
    }

    /*@
    Ox.crossesDateline <f> Returns true if a given line crosses the dateline
        > Ox.crossesDateline({lat: 0, lng: -90}, {lat: 0, lng: 90})
        false
        > Ox.crossesDateline({lat: 0, lng: 90}, {lat: 0, lng: -90})
        true
    @*/
    Ox.crossesDateline = function(pointA, pointB) {
        return pointA.lng > pointB.lng;
    };

    /*@
    Ox.getArea <f> Returns the area in square meters of a given rectancle
    @*/
    Ox.getArea = function(pointA, pointB) {
        /*
        area of a ring between two latitudes:
        2 * PI * r^2 * abs(sin(latA) - sin(latB))
        see http://mathforum.org/library/drmath/view/63767.html
        =>
        2 * Math.PI
        * Math.pow(Ox.EARTH_RADIUS, 2)
        * Math.abs(Math.sin(Ox.rad(latA)) - Math.sin(Ox.rad(latB)))
        * Math.abs(Ox.rad(lngA) - Ox.rad(lngB))
        / (2 * Math.PI)
        */
        if (Ox.crossesDateline(pointA, pointB)) {
            pointB.lng += 360;
        }
        pointA = rad(pointA);
        pointB = rad(pointB);
        return Math.pow(Ox.EARTH_RADIUS, 2)
            * Math.abs(Math.sin(pointA.lat) - Math.sin(pointB.lat))
            * Math.abs(pointA.lng - pointB.lng);
    };

    /*@
    Ox.getBearing <f> Returns the bearing from one point to another
        > Ox.getBearing({lat: -45, lng: 0}, {lat: 45, lng: 0})
        0
        > Ox.getBearing({lat: 0, lng: -90}, {lat: 0, lng: 90})
        90
    @*/
    Ox.getBearing = function(pointA, pointB) {
        var pointA = rad(pointA),
            pointB = rad(pointB),
            x = Math.cos(pointA.lat) * Math.sin(pointB.lat)
                - Math.sin(pointA.lat) * Math.cos(pointB.lat)
                * Math.cos(pointB.lng - pointA.lng),
            y = Math.sin(pointB.lng - pointA.lng)
                * Math.cos(pointB.lat);
        return (Ox.deg(Math.atan2(y, x)) + 360) % 360;
    };

    /*@
    Ox.getCenter <f> Returns the center of a recangle on a spehre
        > Ox.getCenter({lat: -45, lng: -90}, {lat: 45, lng: 90})
        {lat: 0, lng: 0}
    @*/
    Ox.getCenter = function(pointA, pointB) {
        var pointA = rad(pointA),
            pointB = rad(pointB),
            x = Math.cos(pointB.lat)
                * Math.cos(pointB.lng - pointA.lng),
            y = Math.cos(pointB.lat)
                * Math.sin(pointB.lng - pointA.lng),
            d = Math.sqrt(
                Math.pow(Math.cos(pointA.lat) + x, 2) + Math.pow(y, 2)
            ),
            lat = Math.atan2(Math.sin(pointA.lat) + Math.sin(pointB.lat), d),
            lng = pointA.lng + Math.atan2(y, Math.cos(pointA.lat) + x);
        return deg({lat: lat, lng: lng});
    };

    /*@
    Ox.getCircle <f> Returns points on a circle around a given point
        (center, radius, precision) -> <a> Points
        center    <o> Center point ({lat, lng})
        radius    <n> Radius in meters
        precision <n> Precision (the circle will have 2^precision segments)
    @*/
    Ox.getCircle = function(center, radius, precision) {
        return Ox.range(
            0, 360, 360 / Math.pow(2, precision)
        ).map(function(bearing) {
            return Ox.getPoint(center, radius, bearing);
        });
    };

    /*@
    Ox.getDegreesPerMeter <f> Returns degrees per meter at a given latitude
        > 360 / Ox.getDegreesPerMeter(0)
        Ox.EARTH_CIRCUMFERENCE
    @*/
    Ox.getDegreesPerMeter = function(lat) {
        return 360 / Ox.EARTH_CIRCUMFERENCE / Math.cos(lat * Math.PI / 180);
    };

    /*@
    Ox.getDistance <f> Returns the distance in meters between two points
        > Ox.getDistance({lat: -45, lng: -90}, {lat: 45, lng: 90}) * 2
        Ox.EARTH_CIRCUMFERENCE
    @*/
    Ox.getDistance = function(pointA, pointB) {
        var pointA = rad(pointA),
            pointB = rad(pointB);
        return Math.acos(
    	    Math.sin(pointA.lat) * Math.sin(pointB.lat)
    	    + Math.cos(pointA.lat) * Math.cos(pointB.lat)
    	    * Math.cos(pointB.lng - pointA.lng)
    	) * Ox.EARTH_RADIUS;
    };

    /*@
    Ox.getLatLngByXY <f> Returns lat/lng for a given x/y on a 1x1 mercator projection
        > Ox.getLatLngByXY({x: 0.5, y: 0.5})
        {lat: 0, lng: 0}
    @*/
    Ox.getLatLngByXY = function(xy) {
        function getVal(val) {
            return (val - 0.5) * 2 * Math.PI;
        }
        return {
            lat: -Ox.deg(Math.atan(Ox.sinh(getVal(xy.y)))),
            lng: Ox.deg(getVal(xy.x))
        };
    };

    /*@
    Ox.getLine <f> Returns points on a line between two points
        (pointA, pointB, precision) -> <a> Points
        pointA    <o> Start point ({lat, lng})
        pointB    <o> End point ({lat, lng})
        precision <n> Precision (the line will have 2^precision segments)
    @*/
    Ox.getLine = function(pointA, pointB, precision) {
        var line = [pointA, pointB], points;
        while (precision > 0) {
            points = [line[0]];
            Ox.loop(line.length - 1, function(i) {
                points.push(
                    Ox.getCenter(line[i], line[i + 1]),
                    line[i + 1]
                );
            });
            line = points;
            precision--;
        }
        return line;
    };

    /*@
    Ox.getMetersPerDegree <f> Returns meters per degree at a given latitude
        > Ox.getMetersPerDegree(0) * 360
        Ox.EARTH_CIRCUMFERENCE
    @*/
    Ox.getMetersPerDegree = function(lat) {
        return Math.cos(lat * Math.PI / 180) * Ox.EARTH_CIRCUMFERENCE / 360;
    };

    /*@
    Ox.getPoint <f> Returns a point at a given distance/bearing from a given point
        > Ox.getPoint({lat: -45, lng: 0}, Ox.EARTH_CIRCUMFERENCE / 4, 0)
        {lat: 45, lng: 0}
    @*/
    Ox.getPoint = function(point, distance, bearing) {
        var pointB = {};
        point = rad(point);
        distance /= Ox.EARTH_RADIUS;
        bearing = Ox.rad(bearing);
        pointB.lat = Math.asin(
            Math.sin(point.lat) * Math.cos(distance)
            + Math.cos(point.lat) * Math.sin(distance) * Math.cos(bearing)
        );
        pointB.lng = point.lng + Math.atan2(
            Math.sin(bearing) * Math.sin(distance) * Math.cos(point.lat),
            Math.cos(distance) - Math.sin(point.lat) * Math.sin(pointB.lat)
        );
        return deg(pointB);
    };

    /*@
    Ox.getXYByLatLng <f> Returns x/y on a 1x1 mercator projection for a given lat/lng
        > Ox.getXYByLatLng({lat: 0, lng: 0})
        {x: 0.5, y: 0.5}
    @*/
    Ox.getXYByLatLng = function(latlng) {
        function getVal(val) {
            return (val / (2 * Math.PI) + 0.5)
        }
        return {
            x: getVal(Ox.rad(latlng.lng)),
            y: getVal(Ox.asinh(Math.tan(Ox.rad(-latlng.lat))))
        };
    };

    /*@
    Ox.isPolar <f> Returns true if a given point is outside the bounds of a mercator projection
        > Ox.isPolar({lat: 90, lng: 0})
        true
    @*/
    Ox.isPolar = function(point) {
        return point.lat < Ox.MIN_LATITUDE || point.lat > Ox.MAX_LATITUDE;
    };

}());

//@ Ox.Line <f> (undocumented)
Ox.Line = function(pointA, pointB) {

    var self = {
            points: [pointA, pointB]
        },
        that = this;

    function rad() {
        return self.points.map(function(point) {
            return {
                lat: Ox.rad(point.lat()),
                lng: Ox.rad(point.lng())
            };
        });
    }

    that.getArea = function() {
        
    };

    that.getBearing = function() {
    };

    that.getDistance = function() {
        var points = rad();
        return Math.acos(
    	    Math.sin(point[0].lat) * Math.sin(point[1].lat) + 
    	    Math.cos(point[0].lat) * Math.cos(point[1].lat) *
    	    Math.cos(point[1].lng - point[0].lng)
    	) * Ox.EARTH_RADIUS; 
    };

    that.getMidpoint = function() {
        var points = rad(),
            x = Math.cos(point[1].lat) *
                Math.cos(point[1].lng - point[0].lng),
            y = Math.cos(point[1].lat) *
                Math.sin(point[1].lng - point[0].lng),
            d = Math.sqrt(
                Math.pow(Math.cos(point[0].lat) + x, 2) + Math.pow(y, 2)
            ),
            lat = Ox.deg(
                Math.atan2(Math.sin(points[0].lat) + Math.sin(points[1].lat), d)
            ),
            lng = Ox.deg(
                points[0].lng + Math.atan2(y, math.cos(points[0].lat) + x)
            );
        return new Point(lat, lng);
    };

    that.points = function() {
        
    };

    return that;

};

//@ Ox.Point <f> (undocumented)
Ox.Point = function(lat, lng) {

    var self = {lat: lat, lng: lng},
        that = this;

    that.lat = function() {
        
    };

    that.latlng = function() {
        
    };

    that.lng = function() {
        
    };

    that.getMetersPerDegree = function() {
        return Math.cos(self.lat * Math.PI / 180) *
            Ox.EARTH_CIRCUMFERENCE / 360;
    }

    that.getXY = function() {
        return [
            getXY(Ox.rad(self.lng)),
            getXY(Ox.asinh(Math.tan(Ox.rad(-self.lat))))
        ];
    };

    return that;

};

//@ Ox.Rectangle <f> (undocumented)
Ox.Rectangle = function(pointA, pointB) {

    var self = {
            points: [
                new Point(
                    Math.min(pointA.lat(), pointB.lat()),
                    pointA.lng()
                ),
                new Point(
                    Math.max(pointA.lat(), pointB.lat()),
                    pointB.lng()
                )
            ]
        },
        that = this;

    that.contains = function(rectangle) {
        
    };

    that.crossesDateline = function() {
        return self.points[0].lng > self.points[1].lng;
    }

    that.getCenter = function() {
        return new Ox.Line(self.points[0], self.points[1]).getMidpoint();
    };

    that.intersects = function(rectangle) {
        
    };

    return that;

};