/*global console, document, window*/
/** @license
 *
 *  Copyright (C) 2012 K. Arthur Endsley (kaendsle@mtu.edu)
 *  Michigan Tech Research Institute (MTRI)
 *  3600 Green Court, Suite 100, Ann Arbor, MI, 48105
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/**
 * @desc The Wkt namespace.
 * @property    {String}    delimiter   - The default delimiter for separating components of atomic geometry (coordinates)
 * @namespace
 * @global
 */
var Wkt = (function () { // Execute function immediately
    var beginsWith, endsWith;

    /**
     * Returns true if the substring is found at the beginning of the string.
     * @param   str {String}    The String to search
     * @param   sub {String}    The substring of interest
     * @return      {Boolean}
     * @private
     */
    beginsWith = function (str, sub) {
        return str.substring(0, sub.length) === sub;
    };

    /**
     * Returns true if the substring is found at the end of the string.
     * @param   str {String}    The String to search
     * @param   sub {String}    The substring of interest
     * @return      {Boolean}
     * @private
     */
    endsWith = function (str, sub) {
        return str.substring(str.length - sub.length) === sub;
    };

    return {
        /**
         * The default delimiter for separating components of atomic geometry (coordinates)
         * @ignore
         */
        delimiter: ' ',

        /**
         * Determines whether or not the passed Object is an Array.
         * @param   obj {Object}    The Object in question
         * @return      {Boolean}
         * @member Wkt.isArray
         * @method
         */
        isArray: function (obj) {
            return !!(obj && obj.constructor === Array);
        },

        /**
         * Removes given character String(s) from a String.
         * @param   str {String}    The String to search
         * @param   sub {String}    The String character(s) to trim
         * @return      {String}    The trimmed string
         * @member Wkt.trim
         * @method
         */
        trim: function (str, sub) {
            sub = sub || ' '; // Defaults to trimming spaces
            // Trim beginning spaces
            while (beginsWith(str, sub)) {
                str = str.substring(1);
            }
            // Trim ending spaces
            while (endsWith(str, sub)) {
                str = str.substring(0, str.length - 1);
            }
            return str;
        },

        /**
         * An object for reading WKT strings and writing geographic features
         * @constructor Wkt.Wkt
         * @param   initializer {String}    An optional WKT string for immediate read
         * @property            {Array}     components      - Holder for atomic geometry objects (internal representation of geometric components)
         * @property            {String}    delimiter       - The default delimiter for separating components of atomic geometry (coordinates)
         * @property            {Object}    regExes         - Some regular expressions copied from OpenLayers.Format.WKT.js
         * @property            {String}    type            - The Well-Known Text name (e.g. 'point') of the geometry
         * @property            {Boolean}   wrapVerticies   - True to wrap vertices in MULTIPOINT geometries; If true: MULTIPOINT((30 10),(10 30),(40 40)); If false: MULTIPOINT(30 10,10 30,40 40)
         * @return              {Wkt.Wkt}
         * @memberof Wkt
         */
        Wkt: function (initializer) {

            /**
             * The default delimiter between X and Y coordinates.
             * @ignore
             */
            this.delimiter = Wkt.delimiter || ' ';

            /**
             * Configuration parameter for controlling how Wicket seralizes
             * MULTIPOINT strings. Examples; both are valid WKT:
             * If true: MULTIPOINT((30 10),(10 30),(40 40))
             * If false: MULTIPOINT(30 10,10 30,40 40)
             * @ignore
             */
            this.wrapVertices = true;

            /**
             * Some regular expressions copied from OpenLayers.Format.WKT.js
             * @ignore
             */
            this.regExes = {
                'typeStr': /^\s*(\w+)\s*\(\s*(.*)\s*\)\s*$/,
                'spaces': /\s+|\+/, // Matches the '+' or the empty space
                'numeric': /-*\d+(\.*\d+)?/,
                'comma': /\s*,\s*/,
                'parenComma': /\)\s*,\s*\(/,
                'coord': /-*\d+\.*\d+ -*\d+\.*\d+/, // e.g. "24 -14"
                'doubleParenComma': /\)\s*\)\s*,\s*\(\s*\(/,
                'trimParens': /^\s*\(?(.*?)\)?\s*$/,
                'ogcTypes': /^(multi)?(point|line|polygon|box)?(string)?$/i, // Captures e.g. "Multi","Line","String"
				'crudeJson': /^{.*"(type|coordinates|geometries|features)":.*}$/ // Attempts to recognize JSON strings
            };

            /**
             * The internal representation of geometry--the "components" of geometry.
             * @ignore
             */
            this.components = undefined;

            // An initial WKT string may be provided
            if (initializer && typeof initializer === 'string') {
                this.read(initializer);
            } else if (initializer && typeof initializer !== undefined) {
                this.fromObject(initializer);
            } 

        }

    };

}());

/**
 * Returns true if the internal geometry is a collection of geometries.
 * @return  {Boolean}   Returns true when it is a collection
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.isCollection = function () {
    switch (this.type.slice(0, 5)) {
    case 'multi':
        // Trivial; any multi-geometry is a collection
        return true;
    case 'polyg':
        // Polygons with holes are "collections" of rings
        return true;
    default:
        // Any other geometry is not a collection
        return false;
    }
};

/**
 * Compares two x,y coordinates for equality.
 * @param   a   {Object}    An object with x and y properties
 * @param   b   {Object}    An object with x and y properties
 * @return      {Boolean}
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.sameCoords = function (a, b) {
    return (a.x === b.x && a.y === b.y);
};

/**
 * Sets internal geometry (components) from framework geometry (e.g.
 * Google Polygon objects or google.maps.Polygon).
 * @param   obj {Object}    The framework-dependent geometry representation
 * @return      {Wkt.Wkt}   The object itself
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.fromObject = function (obj) {
    var result;

	if (obj.hasOwnProperty('type') && obj.hasOwnProperty('coordinates')) {
		result = this.fromJson(obj);
	} else {
		result = this.deconstruct.call(this, obj);
	}

    this.components = result.components;
    this.isRectangle = result.isRectangle || false;
    this.type = result.type;
    return this;
};

/**
 * Creates external geometry objects based on a plug-in framework's
 * construction methods and available geometry classes.
 * @param   config  {Object}    An optional framework-dependent properties specification
 * @return          {Object}    The framework-dependent geometry representation
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.toObject = function (config) {
    return this.construct[this.type].call(this, config);
};

/**
 * Returns the WKT string representation; the same as the write() method.
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.toString = function (config) {
    return this.write();
};

/**
 * Parses a JSON representation as an Object.
 * @param	obj	{Object}	An Object with the GeoJSON schema
 * @return    	{Wkt.Wkt}	The object itself
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.fromJson = function (obj) {
	var i, j, k, coords, iring, oring;

	this.type = obj.type.toLowerCase();
	this.components = [];

	coords = obj.coordinates;

	if (!Wkt.isArray(coords[0])) { // Point
		this.components.push({
			x: coords[0],
			y: coords[1]
		});

	} else {

		for (i in coords) {
			if (coords.hasOwnProperty(i)) {

				if (!Wkt.isArray(coords[i][0])) { // LineString

					if (this.type === 'multipoint') { // MultiPoint
						this.components.push([{
							x: coords[i][0],
							y: coords[i][1]
						}]);

					} else {
						this.components.push({
							x: coords[i][0],
							y: coords[i][1]
						});

					}

				} else {

					oring = [];
					for (j in coords[i]) {
						if (coords[i].hasOwnProperty(j)) {

							if (!Wkt.isArray(coords[i][j][0])) {
								oring.push({
									x: coords[i][j][0],
									y: coords[i][j][1]
								});

							} else {

								iring = [];
								for (k in coords[i][j]) {
									if (coords[i][j].hasOwnProperty(k)) {

										iring.push({
											x: coords[i][j][k][0],
											y: coords[i][j][k][1]
										});

									}
								}

								oring.push(iring);

							}

						}
					}

					this.components.push(oring);
				}
			}
		}

	}

	return this;
};

/**
 * Creates a JSON representation, with the GeoJSON schema, of the geometry.
 * @return    {Object}    The corresponding GeoJSON representation
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.toJson = function () {
	var cs, json, i, j, k, ring, rings;

	cs = this.components;
    json = {
    	coordinates: [],
		type: (function () {    
			var i, type, s;
		
			type = this.regExes.ogcTypes.exec(this.type).slice(1);
			s = [];
			
			for (i in type) {
				if (type.hasOwnProperty(i)) {
					if (type[i] !== undefined) {
						s.push(type[i].toLowerCase().slice(0,1).toUpperCase()
							+ type[i].toLowerCase().slice(1));
					}
				}
			}
			
			return s;
		}.call(this)).join('')
	}

	// Wkt BOX type gets a special bbox property in GeoJSON
	if (this.type.toLowerCase() === 'box') {
		json.type = 'Polygon';
		json.bbox = [];
		
		for (i in cs) {
			if (cs.hasOwnProperty(i)) {
				json.bbox = json.bbox.concat([cs[i].x, cs[i].y]);
			}
		}
		
		json.coordinates = [[
			[cs[0].x, cs[0].y],
			[cs[0].x, cs[1].y],
			[cs[1].x, cs[1].y],
			[cs[1].x, cs[0].y],
			[cs[0].x, cs[0].y]
		]];

		return json;
	}
	
	// For the coordinates of most simple features
	for (i in cs) {
		if (cs.hasOwnProperty(i)) {

			// For those nested structures
			if (Wkt.isArray(cs[i])) {
				rings = [];

				for (j in cs[i]) {
					if (cs[i].hasOwnProperty(j)) {

						if (Wkt.isArray(cs[i][j])) { // MULTIPOLYGONS
							ring = [];
						
							for (k in cs[i][j]) {
								if (cs[i][j].hasOwnProperty(k)) {
									ring.push([cs[i][j][k].x, cs[i][j][k].y]);
								}
							}
							
							rings.push(ring);

						} else { // POLYGONS and MULTILINESTRINGS
							
							if (cs[i].length > 1) {
								rings.push([cs[i][j].x, cs[i][j].y]);
								
							} else { // MULTIPOINTS
								rings = rings.concat([cs[i][j].x, cs[i][j].y]);
							}
						}
					}
				}
				
				json.coordinates.push(rings);
				
			} else {
				if (cs.length > 1) { // For LINESTRING type
					json.coordinates.push([cs[i].x, cs[i].y]);
					
				} else { // For POINT type
					json.coordinates = json.coordinates.concat([cs[i].x, cs[i].y]);
				}
			}

		}
	}
	
    return json;
 };

/**
 * Absorbs the geometry of another Wkt.Wkt instance, merging it with its own,
 * creating a collection (MULTI-geometry) based on their types, which must agree.
 * For example, creates a MULTIPOLYGON from a POLYGON type merged with another
 * POLYGON type, or adds a POLYGON instance to a MULTIPOLYGON instance.
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.merge = function (wkt) {
    var prefix = this.type.slice(0, 5);

    if (this.type !== wkt.type) {
        if (this.type.slice(5, this.type.length) !== wkt.type) {
            throw TypeError('The input geometry types must agree or the calling Wkt.Wkt instance must be a multigeometry of the other');
        }
    }

    switch (prefix) {

        case 'point':
        this.components = [this.components.concat(wkt.components)];
        break;

        case 'multi':
        this.components = this.components.concat((wkt.type.slice(0, 5) === 'multi') ? wkt.components : [wkt.components]);
        break;

        default:
        this.components = [
            this.components,
            wkt.components
        ]
        break;

    }

    if (prefix !== 'multi') {
        this.type = 'multi' + this.type;
    }
};

/**
 * Reads a WKT string, validating and incorporating it.
 * @param   str {String}    A WKT or GeoJSON string
 * @return      {Array}     An Array of internal geometry objects
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.read = function (str) {
    var matches;
    matches = this.regExes.typeStr.exec(str);
    if (matches) {
        this.type = matches[1].toLowerCase();
        this.base = matches[2];
        if (this.ingest[this.type]) {
            this.components = this.ingest[this.type].apply(this, [this.base]);
        }

    } else {
		if (this.regExes.crudeJson.test(str)) {
			if (typeof JSON === 'object' && typeof JSON.parse === 'function') {
				this.fromJson(JSON.parse(str));

			} else {
				console.log('JSON.parse() is not available; cannot parse GeoJSON strings');
				throw {
					name: 'JSONError',
					message: 'JSON.parse() is not available; cannot parse GeoJSON strings'
				};
			}

		} else {
			console.log('Invalid WKT string provided to read()');
			throw {
				name: 'WKTError',
				message: 'Invalid WKT string provided to read()'
			};
		}
    }

    return this.components;
}; // eo readWkt

/**
 * Writes a WKT string.
 * @param   components  {Array}     An Array of internal geometry objects
 * @return              {String}    The corresponding WKT representation
 * @memberof Wkt.Wkt
 * @method
 */
Wkt.Wkt.prototype.write = function (components) {
    var i, pieces, data;

    components = components || this.components;

    pieces = [];

    pieces.push(this.type.toUpperCase() + '(');

    for (i = 0; i < components.length; i += 1) {
        if (this.isCollection() && i > 0) {
            pieces.push(',');
        }

        // There should be an extract function for the named type
        if (!this.extract[this.type]) {
            return null;
        }

        data = this.extract[this.type].apply(this, [components[i]]);
        if (this.isCollection() && this.type !== 'multipoint') {
            pieces.push('(' + data + ')');

        } else {
            pieces.push(data);

            // If not at the end of the components, add a comma
            if (i !== (components.length - 1) && this.type !== 'multipoint') {
                pieces.push(',');
            }

        }
    }

    pieces.push(')');

    return pieces.join('');
};

/**
 * This object contains functions as property names that extract WKT
 * strings from the internal representation.
 * @memberof Wkt.Wkt
 * @namespace Wkt.Wkt.extract
 * @instance
 */
Wkt.Wkt.prototype.extract = {
    /**
     * Return a WKT string representing atomic (point) geometry
     * @param   point   {Object}    An object with x and y properties
     * @return          {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    point: function (point) {
        return String(point.x) + this.delimiter + String(point.y);
    },

    /**
     * Return a WKT string representing multiple atoms (points)
     * @param   multipoint  {Array}     Multiple x-and-y objects
     * @return              {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    multipoint: function (multipoint) {
        var i, parts = [], s;

        for (i = 0; i < multipoint.length; i += 1) {
            s = this.extract.point.apply(this, [multipoint[i]]);

            if (this.wrapVertices) {
                s = '(' + s + ')';
            }

            parts.push(s);
        }

        return parts.join(',');
    },

    /**
     * Return a WKT string representing a chain (linestring) of atoms
     * @param   linestring  {Array}     Multiple x-and-y objects
     * @return              {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    linestring: function (linestring) {
        // Extraction of linestrings is the same as for points
        return this.extract.point.apply(this, [linestring]);
    },

    /**
     * Return a WKT string representing multiple chains (multilinestring) of atoms
     * @param   multilinestring {Array}     Multiple of multiple x-and-y objects
     * @return                  {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    multilinestring: function (multilinestring) {
        var i, parts = [];

        for (i = 0; i < multilinestring.length; i += 1) {
            parts.push(this.extract.linestring.apply(this, [multilinestring[i]]));
        }

        return parts.join(',');
    },

    /**
     * Return a WKT string representing multiple atoms in closed series (polygon)
     * @param   polygon {Array}     Collection of ordered x-and-y objects
     * @return          {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    polygon: function (polygon) {
        // Extraction of polygons is the same as for multilinestrings
        return this.extract.multilinestring.apply(this, [polygon]);
    },

    /**
     * Return a WKT string representing multiple closed series (multipolygons) of multiple atoms
     * @param   multipolygon    {Array}     Collection of ordered x-and-y objects
     * @return                  {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    multipolygon: function (multipolygon) {
        var i, parts = [];
        for (i = 0; i < multipolygon.length; i += 1) {
            parts.push('(' + this.extract.polygon.apply(this, [multipolygon[i]]) + ')');
        }
        return parts.join(',');
    },

    /**
     * Return a WKT string representing a 2DBox
     * @param   multipolygon    {Array}     Collection of ordered x-and-y objects
     * @return                  {String}    The WKT representation
     * @memberof Wkt.Wkt.extract
     * @instance
     */
    box: function (box) {
        return this.extract.linestring.apply(this, [box]);
    },

    geometrycollection: function (str) {
        console.log('The geometrycollection WKT type is not yet supported.');
    }
};

/**
 * This object contains functions as property names that ingest WKT
 * strings into the internal representation.
 * @memberof Wkt.Wkt
 * @namespace Wkt.Wkt.ingest
 * @instance
 */
Wkt.Wkt.prototype.ingest = {

    /**
     * Return point feature given a point WKT fragment.
     * @param   str {String}    A WKT fragment representing the point
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    point: function (str) {
        var coords = Wkt.trim(str).split(this.regExes.spaces);
        // In case a parenthetical group of coordinates is passed...
        return [{ // ...Search for numeric substrings
            x: parseFloat(this.regExes.numeric.exec(coords[0])[0]),
            y: parseFloat(this.regExes.numeric.exec(coords[1])[0])
        }];
    },

    /**
     * Return a multipoint feature given a multipoint WKT fragment.
     * @param   str {String}    A WKT fragment representing the multipoint
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    multipoint: function (str) {
        var i, components, points;
        components = [];
        points = Wkt.trim(str).split(this.regExes.comma);
        for (i = 0; i < points.length; i += 1) {
            components.push(this.ingest.point.apply(this, [points[i]]));
        }
        return components;
    },

    /**
     * Return a linestring feature given a linestring WKT fragment.
     * @param   str {String}    A WKT fragment representing the linestring
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    linestring: function (str) {
        var i, multipoints, components;

        // In our x-and-y representation of components, parsing
        //  multipoints is the same as parsing linestrings
        multipoints = this.ingest.multipoint.apply(this, [str]);

        // However, the points need to be joined
        components = [];
        for (i = 0; i < multipoints.length; i += 1) {
            components = components.concat(multipoints[i]);
        }
        return components;
    },

    /**
     * Return a multilinestring feature given a multilinestring WKT fragment.
     * @param   str {String}    A WKT fragment representing the multilinestring
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    multilinestring: function (str) {
        var i, components, line, lines;
        components = [];

        lines = Wkt.trim(str).split(this.regExes.doubleParenComma);
        if (lines.length === 1) { // If that didn't work...
            lines = Wkt.trim(str).split(this.regExes.parenComma);
        }

        for (i = 0; i < lines.length; i += 1) {
            line = lines[i].replace(this.regExes.trimParens, '$1');
            components.push(this.ingest.linestring.apply(this, [line]));
        }

        return components;
    },

    /**
     * Return a polygon feature given a polygon WKT fragment.
     * @param   str {String}    A WKT fragment representing the polygon
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    polygon: function (str) {
        var i, j, components, subcomponents, ring, rings;
        rings = Wkt.trim(str).split(this.regExes.parenComma);
        components = []; // Holds one or more rings
        for (i = 0; i < rings.length; i += 1) {
            ring = rings[i].replace(this.regExes.trimParens, '$1').split(this.regExes.comma);
            subcomponents = []; // Holds the outer ring and any inner rings (holes)
            for (j = 0; j < ring.length; j += 1) {
                // Split on the empty space or '+' character (between coordinates)
                subcomponents.push({
                    x: parseFloat(ring[j].split(this.regExes.spaces)[0]),
                    y: parseFloat(ring[j].split(this.regExes.spaces)[1])
                });
            }
            components.push(subcomponents);
        }
        return components;
    },

    /**
     * Return box vertices (which would become the Rectangle bounds) given a Box WKT fragment.
     * @param   str {String}    A WKT fragment representing the box
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    box: function (str) {
        var i, multipoints, components;

        // In our x-and-y representation of components, parsing
        //  multipoints is the same as parsing linestrings
        multipoints = this.ingest.multipoint.apply(this, [str]);

        // However, the points need to be joined
        components = [];
        for (i = 0; i < multipoints.length; i += 1) {
            components = components.concat(multipoints[i]);
        }

        return components;
    },

    /**
     * Return a multipolygon feature given a multipolygon WKT fragment.
     * @param   str {String}    A WKT fragment representing the multipolygon
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    multipolygon: function (str) {
        var i, components, polygon, polygons;
        components = [];
        polygons = Wkt.trim(str).split(this.regExes.doubleParenComma);
        for (i = 0; i < polygons.length; i += 1) {
            polygon = polygons[i].replace(this.regExes.trimParens, '$1');
            components.push(this.ingest.polygon.apply(this, [polygon]));
        }
        return components;
    },

    /**
     * Return an array of features given a geometrycollection WKT fragment.
     * @param   str {String}    A WKT fragment representing the geometry collection
     * @memberof Wkt.Wkt.ingest
     * @instance
     */
    geometrycollection: function (str) {
        console.log('The geometrycollection WKT type is not yet supported.');
    }

}; // eo ingest


