using System;
using System.Collections.Generic;
using Spherical;

/*=====================================================================

  File:      Parser.cs for Spatial Sample
  Summary:   Implements a parser which converts text to various kinds of regions.
  Date:	     August 16, 2005

---------------------------------------------------------------------
  This spatial search library was developed by Alexander Szalay, Robert Brunner, 
  Peter Kunszt, and Gyorgy Fekete at the Department of Physics and Astronomy, 
  The Johns Hopkins University, in collaboration with Jim Gray, Microsoft Research.
  Details of the algorithms can be found at http://www.sdss.jhu.edu/htm/.
 
======================================================= */

namespace Spherical.HTM
{
	/// <summary>
	/// Turns text specifications into Regions objects
	/// </summary>
	public class Parser {
		private enum Format {
			J2000,
			Cartesian,
			Latlon,
			Null,
			Unkown
		}
		private enum Geometry {
			Region,
			Convex,
			Halfspace, /* n/a */
			Rect,
			Circle,
			Poly,
			Chull,
			Null
		}
		private enum Error {
			Ok,
			errLevel,
			errIllegalNumber,
			errFormat,
			errEol,
			errNot2forRect,
			errNotenough4Poly,
			errMissingConvex,
			errPolyBowtie,
			errPolyZeroLength,
			errPolyToomanyPoints,
			errBadedgePoly,
			errChullTooBig,
			errNullSpecification,
			errUnknown
		}
		private const int INITIAL_CAPACITY = 100;
		private int _current_capacity = INITIAL_CAPACITY;
		private List<double> xs, ys, zs, ras, decs;
		private string _spec;
		private char[] _delims;
		private string[] _tokens;
		private int _ct;
		private Error _error;
		private Format _format;
		private Geometry _geometry; // The kind of object we are building
		private Region _targetregion;		// Everything is a region, it will be built to here....
		private bool beginAccumulate;

		private void init(){
			_delims = " \t\n\r".ToCharArray();
			xs  = new List<double>(INITIAL_CAPACITY); // Should be made to grow eventually
			ys = new List<double>(INITIAL_CAPACITY);
			zs = new List<double>(INITIAL_CAPACITY);
			ras = new List<double>(INITIAL_CAPACITY);
			decs = new List<double>(INITIAL_CAPACITY);
			beginAccumulate = false;
		}
		private string __prematureeol = "end of line not expected. maybe need more arguments?";
		/// <summary>
		/// Returns the text of the error message associated with the most recent error
		/// </summary>
		/// <returns></returns>
		private string errmsg(){
			switch(_error){
				case Error.Ok: return "Ok" ;
				case Error.errLevel: return "Level specification error";
				case Error.errIllegalNumber: return "Illegal number format"; 
				case Error.errFormat: return "Format must be CARTESIAN, J2000 or LATLON"; 
				case Error.errUnknown: return "Unrecognized area name, or syntax error";
				case Error.errEol: return this.__prematureeol;
				case Error.errNot2forRect: return "Not 2 points given (exactly 2 needed) for a rectangle)";
				case Error.errMissingConvex: return "Missing a keyword: 'CONVEX'";
				case Error.errPolyBowtie: return "Polygon has a bowtie or is concave";
				case Error.errPolyToomanyPoints: return "Too many points in polygon";
				case Error.errPolyZeroLength: return "Polygon had zero length edges";
				case Error.errNotenough4Poly: return "Need 3 or more points for a polygon";
				case Error.errChullTooBig: return "Points for convex hull cover more than half the globe";
				case Error.errNullSpecification: return "Empty string is an incorrect specification";
			}
			return "Unknown error (3). Call George";
		}
		private Parser() {
			//
			// TODO: Add constructor logic here
			//
			init();
			input = null;
		}
		private Region region {
			get {
				return this._targetregion;
			}
			set {
				this._targetregion = value;
			}
		}
		/* *******************process tokens******************************/
		private void advance(){
			_ct++;
		}
		private  bool isok {
			get {
				return _error == Error.Ok;
			}
		}
		/// <summary>
		/// True, if there are any tokens left to parse. ismore() skips over white spaces
		/// </summary>
		private  bool ismore {
			get {
				if (_tokens == null)
					return false;
				if (_tokens[0] == null)
					return false;
/* is: */
				while (_ct < _tokens.Length && _tokens[_ct].Length == 0) {
					_ct++;
				}
				if (_ct < _tokens.Length && beginAccumulate) {
					try {
						double.Parse(_tokens[_ct]);
					} catch {
						return false;
					}
				}
/*WAS:*/
				//while( _ct < _tokens.Length && _tokens[_ct].Length == 0){
				//    _ct++;
				//}
				return _ct < _tokens.Length;
			}
		}
		private int getint(int skip){
			int res;
			if (!ismore){
				_error = Error.errEol;
				throw new ApplicationException(__prematureeol);
			}
			res = int.Parse(_tokens[_ct].Substring(skip));
			advance();
			return res;
		}
		private int getint() {
			int res;
			if (!ismore){
				_error = Error.errEol;
				throw new ApplicationException(__prematureeol);
			}
			res = int.Parse(_tokens[_ct]);
			advance();
			return res;
		}
		private double getdouble() {
			double res;
			if (!ismore){
				_error = Error.errEol;
				throw new Exception(__prematureeol);
			}
			res = double.Parse(_tokens[_ct]);
			advance();
			return res;
		}
        private void getNormalVector(out double x, out double y, out double z) {
            x = getdouble();
            y = getdouble();
            z = getdouble();
            double norm = x * x + y * y + z * z;
            norm = Math.Sqrt(norm);
            x /= norm;
            y /= norm;
            z /= norm;
            return;
        }
		private bool match_current(string pattern, bool casesensitive){
			if (!ismore){
				return false;
			}
			if (casesensitive){
				return (_tokens[_ct].Equals(pattern));
			} else {
				return (_tokens[_ct].ToLower().Equals(pattern));
			}
		}
		private bool isprefix(string prefix){
			if (!ismore)
				return false;
			return _tokens[_ct].Substring(0, prefix.Length).Equals(prefix);
		}
		private string input {
			get {
				return _spec;
			}
			set {
				_error = Error.Ok;
				_format = Format.Unkown;
				_geometry = Geometry.Null;
				_ct = 0;
				_spec = value;
				_targetregion = null;
				if (_spec == null){
					_tokens = null;
				} else if (_spec.Length > 0){
					_tokens = _spec.Split(_delims);

				} else {
					_tokens = null;
				}		
			}
		}
		private void rewind(){
			_ct = 0;
		}
		private  Parser(char[] in_spec) {
			//
			// Break spec into words (tokens)
			// First word must be recognized as one of the keys
			// Case insensitive
			init();
			input = new string(in_spec);
		}
		private Parser(string in_spec){
			init();
			input = in_spec;
		}
		
		/*************************************                   *************************/
		/*************************************                   *************************/
		/************************************* CONVEX            *************************/
		/************************************* REGION            *************************/
		/*************************************                   *************************/
		/************************************* RECTANGLE         *************************/
		/*************************************                   *************************/
		/************************************* CIRCLE            *************************/
		/*************************************                   *************************/
		/************************************* POLYGON           *************************/
		/*************************************                   *************************/
		/************************************* CHULL             *************************/
		/************************************* convex hull       *************************/
		private Format peekFormat() {
			// Peek into the string, and return the type of geometric object
			// the parser will build.

			if (_tokens == null)
				return Format.Null;
			if (match_current("cartesian", false)) {
				return Format.Cartesian;
			} else if (match_current("j2000", false)) {
				return Format.J2000;
			} else if (match_current("latlon", false)) {
				return Format.Latlon;
			} else {
				return Format.Null;
			}
			// return Format.Null;
		}
		private Geometry peekGeometry() {
			// Peek into the string, and return the type of geometric object
			// the parser will build.
			if (_tokens == null)
				return Geometry.Null;
			if (match_current("convex", false))
				return Geometry.Convex;
			if (match_current("region", false))
				return Geometry.Region;
			if (match_current("halfspace", false))
				return Geometry.Halfspace;
			if (match_current("poly", false))
				return Geometry.Poly;
			if (match_current("chull", false))
				return Geometry.Chull;
			if (match_current("rect", false))
				return Geometry.Rect;
			if (match_current("circle", false))
				return Geometry.Circle;
			return Geometry.Null;
		}
		private bool parse() {
			// smarter parser, allows mixed geometries and object
			// Skip the first 'REGION', else assume that
			bool multiple = false; // parse a region, use multiple, else just one object

			if (!ismore) {
				_error = Error.errNullSpecification;
				return isok;
			}
			_geometry = peekGeometry();
			if (_geometry == Geometry.Region) {
				advance();
				multiple = true;
			}
			while (ismore) {			// We loop on each CONVEX, whatever it may be...
				_geometry = peekGeometry();
                this.crearPoints();
				switch (_geometry) {
					case Geometry.Circle:
						if (parse_circle() == false) {
							return isok; // Adds circle convex to _targetreion
						}
						break;
					case Geometry.Convex:
						if (parse_convex() == false) {
							return isok;
						}
						break;
					case Geometry.Rect:
						if (parse_rectangle() == false) {
							return isok;
						}
						break;
					case Geometry.Poly:
						if (parse_polygon() == false) {
							return isok;
						}
						break;
					case Geometry.Chull:
						if (parse_chull() == false) {
							return isok;
						}
						break;
					case Geometry.Null:
						_error = Error.errUnknown;
						return isok;
						// break;
				}
				if (!multiple)
					break;
			}
			return true;
		}
        private void crearPoints()
        {
            xs.Clear();
            ys.Clear();
            zs.Clear();
            ras.Clear();
            decs.Clear();
        }
		private void increaseCapacity() {
			this._current_capacity += INITIAL_CAPACITY;
			xs.Capacity += INITIAL_CAPACITY;
			ys.Capacity += INITIAL_CAPACITY;
			zs.Capacity += INITIAL_CAPACITY;
			ras.Capacity += INITIAL_CAPACITY;
			decs.Capacity += INITIAL_CAPACITY;
		}

		/************************************* ***************** *************************/
		/************************************* SPECIALTY PARSERS *************************/
		/************************************* ***************** *************************/
		#region parse_RECTANGLE
		/* ******************************************** RECT ***/
		private bool parse_rectangle() {

			Region region = _targetregion;
			int npoints = 0;
			double ra, dec, x, y, z;
            
			if (region == null)
				return true;
			advance(); // Skip over 'RECT'
			_format = peekFormat();
			if (ismore && _format != Format.Null) {
				advance();
			}
			while (ismore) {
				switch (_format) {
					case Format.Null:
					case Format.Cartesian:
						try {
                            //x = this.getdouble();
                            //y = this.getdouble();
                            //z = this.getdouble();
                            getNormalVector(out x, out y, out z);
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						xs.Add(x);
						ys.Add(y);
						zs.Add(z);
						Cartesian.Xyz2Radec(x, y, z, out ra, out dec);
						ras.Add(ra);
						decs.Add(dec);
						npoints++;
						break;
					case Format.J2000:
					case Format.Latlon:
						try {
							ra = this.getdouble();
							dec = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						ras.Add(ra);
						decs.Add(dec);
						npoints++;
						break;
					default:
						break;
				}
				if (npoints >= 2) {
					break;
				}
			}
			if (npoints != 2) {
				_error = Error.errNot2forRect;
				return isok;
			}
            //
            // Rectangle's static methods will create a new convex
            // 
            //
			// WAS:rect = new Rectangle(region);
            Convex con;
			if (_format == Format.Latlon) {
				con = Rectangle.Make(decs[0], ras[0], decs[1], ras[1]);
			} else {
				con = Rectangle.Make(ras[0], decs[0], ras[1], decs[1]);
			}
            _targetregion.Add(con);
			return true;
		}
		/************************** END RECT *******************************/
		#endregion
		#region parse_POLYGON
		/* ******************************************** POLY ***/
		private bool parse_polygon() {

			int npoints = 0;
			double ra, dec, x, y, z;

			if (_targetregion == null)
				return true;

			advance(); // Skip over 'RECT'
			_format = peekFormat();
			if (ismore && _format != Format.Null) {
				advance();
			}
			while (ismore) {
				if (npoints >= this._current_capacity) {
					increaseCapacity();
				}
				switch (_format) {
					case Format.Null:
					case Format.Cartesian:
						try {
                            //x = this.getdouble();
                            //y = this.getdouble();
                            //z = this.getdouble();
                            getNormalVector(out x, out y, out z);
                        } catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						xs.Add(x);
						ys.Add(y);
						zs.Add(z);
						npoints++;
						beginAccumulate = true;
						break;
					case Format.J2000:
					case Format.Latlon:
						try {
							ra = this.getdouble();
							dec = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						if (_format == Format.Latlon) {
							Cartesian.Radec2Xyz(dec, ra, out x, out y, out z);
						} else {
							Cartesian.Radec2Xyz(ra, dec, out x, out y, out z);
						}
						xs.Add(x);
						ys.Add(y);
						zs.Add(z);
						npoints++;
						beginAccumulate = true;
						break;
					default:
						break;
				}
			}
			if (npoints < 3) {
				_error = Error.errNotenough4Poly;
				return isok;
			}
			// poly = new Polygon(region);
			// WARNING!!! more than one kinf of error. The other one is for
			// degenerate edges, two points are too close... or you can eliminate them!
            Polygon.Error err;
            Convex con;
            con = Polygon.Make(xs, ys, zs, npoints, out err);
            if (con != null)
            {
                _targetregion.Add(con);
            }
			// = poly.make(xs, ys, zs, npoints);
			if (err == Polygon.Error.errBowtieOrConcave) {
				_error = Error.errPolyBowtie;
				return isok;
			} else if (err == Polygon.Error.errZeroLength){
				_error = Error.errPolyZeroLength;
				return isok;
			}
			beginAccumulate = false;
			return true;
		}
		/************************** END RECT *******************************/
		#endregion
		#region parse_CHULL
		/* ******************************************** CHULL ***/
		private bool parse_chull() {

			Region region = _targetregion;
			Chull plate = null;
			int npoints = 0;
			double ra, dec, x, y, z;

			if (region == null)
				return true;
			advance(); // Skip over 'RECT'
			_format = peekFormat();
			if (ismore && _format != Format.Null) {
				advance();
			}
			while (ismore) {
				if (npoints >= this._current_capacity) {
					_error = Error.errPolyToomanyPoints;
					return isok;
				}
				switch (_format) {
					case Format.Null:
					case Format.Cartesian:
						try {
                            getNormalVector(out x, out y, out z);
                            //x = this.getdouble();
                            //y = this.getdouble();
                            //z = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						xs.Add(x);
						ys.Add(y);
						zs.Add(z);
						npoints++;
						break;
					case Format.J2000:
					case Format.Latlon:
						try {
							ra = this.getdouble();
							dec = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						if (_format == Format.Latlon) {
							Cartesian.Radec2Xyz(dec, ra, out x, out y, out z);
						} else {
							Cartesian.Radec2Xyz(ra, dec, out x, out y, out z);
						}
						xs.Add(x);
						ys.Add(y);
						zs.Add(z);
						npoints++;
						break;
					default:
						break;
				}
			}
			if (npoints < 3) {
				_error = Error.errNotenough4Poly;
				return isok;
			}
			plate = new Chull(region);
			if (plate.addConvex(xs, ys, zs, npoints) == false) {
				_error = Error.errChullTooBig;
				return isok;
			}
			return true;
		}
		/************************** END CHULL *******************************/
		#endregion
		#region parse_circle
		/* ******************************************** CIRCLE ***/
		private bool parse_circle() {
	
			Region region = _targetregion;
			// Circle circle = null;
            Convex con;
			double ra, dec, x, y, z, rad;

			if (region == null)
				return true;
			advance(); // Skip over 'CIRCLE'
			_format = peekFormat();
			if (ismore && _format != Format.Null) {
				advance();
			}
			if (ismore) {
				switch (_format){
					case Format.Null:
					case Format.Cartesian:
						try {
                            //x = this.getdouble();
                            //y = this.getdouble();
                            //z = this.getdouble();
                            getNormalVector(out x, out y, out z);
							rad = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
                        con = Circle.Make(x, y, z, rad);
                        _targetregion.Add(con);
						break;
					case Format.J2000:
					case Format.Latlon:
						try {
							ra = this.getdouble();
							dec = this.getdouble();
							rad = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						if (_format == Format.J2000) {
                            con = Circle.Make(ra, dec, rad);
						} else {
                            con = Circle.Make(dec, ra, rad);
        
                         }
                        _targetregion.Add(con);
						break;
					default:
						break;
				}
			}
			return true;
		}
		/************************** END CIRCLE *******************************/
		#endregion
		#region parse_convex
		// ////////////////////////////// parse CONVEX
		private bool parse_convex() {

			Region region = _targetregion;
			Convex convex = null;
			Geometry nextitem;
			double ra, dec, x, y, z, D;

			if (region == null)
				return true;
			advance(); // Skip over 'CONVEX'
			_format = peekFormat();
			if (ismore && _format != Format.Null) {
				advance();
			}
			convex = new Convex();
			region.Add(convex);
			while (ismore) {
				nextitem = peekGeometry();
				if (nextitem != Geometry.Null)
					break;
				switch (_format) {
					case Format.Null:
					case Format.Cartesian:
						try {
                            //x = this.getdouble();
                            //y = this.getdouble();
                            //z = this.getdouble();
                            getNormalVector(out x, out y, out z);
							D = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						convex.Add(new Halfspace(x, y, z, false, D));
						break;
					case Format.J2000:
					case Format.Latlon:
						try {
							ra = this.getdouble();
							dec = this.getdouble();
							D = this.getdouble();
						} catch {
							if (_error != Error.errEol)
								_error = Error.errIllegalNumber;
							return isok;
						}
						if (_format == Format.J2000) {
							Cartesian.Radec2Xyz(ra, dec, out x, out y, out z);
						} else {
							Cartesian.Radec2Xyz(dec, ra, out x, out y, out z);
						}
						convex.Add(new Halfspace(x, y, z, false, D));
						break;
					default:
						break;
				}
			}
			return true;
		}
		#endregion
		// <><>
        /// <summary>
        /// Compile a text description of a region into a Region structure
        /// 
        /// If there is an error in the specification, compile will
        /// throw an ApplicationException with an appropriate text message. 
        /// </summary>
        /// <param name="textSpec"></param>
        /// <returns></returns>
		public static Region compile(String textSpec) {
			Region reg = new Region();
			Parser par = new Parser();
			par.input = textSpec;
			par.region = reg;
			if (par.parse() == false) {
				throw new ApplicationException(par.errmsg());
			}
			reg.SimpleSimplify(Const.Tolerance);
			return reg;
		}
	}
}
