/*
 * NPlot - A charting library for .NET
 * 
 * AxesConstraint.cs
 * Copyright (C) 2003-2006 Matt Howlett and others.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. 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.
 * 3. Neither the name of NPlot 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.
 */

using System;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace NPlot
{

	/// <summary>
	/// Classes derived from this abstract base class define and can apply 
	/// some form of constraint to the positioning and length of one or more
	/// of the four axes of a PlotSurface2D.
	/// </summary>
	public abstract class AxesConstraint
	{

		/// <summary>
		/// Defines an AxisConstraint that forces the world length corresponding
		/// to one pixel on the bottom x-axis to be a certain value. 
		/// 
		/// TODO: Allow the pixel world length to be set for the top axis.
		/// </summary>
		public class XPixelWorldLength : AxesConstraint
		{
			private double pWorldLength_ = 0.0f;
			private object holdFixedY_ = null;

			/// <summary>
			/// Constructor, which defines the world pixel length only. Both 
			/// y-axes will be moved by equal amounts in order to force this
			/// constraint.
			/// </summary>
			/// <param name="p">The world pixel length</param>
			public XPixelWorldLength( double p )
			{
				this.pWorldLength_ = p;
			}
		
			/// <summary>
			/// Constructor, which defines the world pixel length together with
			/// the y-axis that should be held constant when forcing this 
			/// constraint [the other y-axis only will be moved].
			/// </summary>
			/// <param name="p">The world pixel length</param>
			/// <param name="holdFixedY">The position of this y-axis will be 
			/// held constant. The other y-axis will be moved in order to 
			/// force the constraint.</param>
			public XPixelWorldLength( double p, PlotSurface2D.YAxisPosition holdFixedY )
			{
				this.pWorldLength_ = p;
				this.holdFixedY_ = holdFixedY;
			}

			/// <summary>
			/// Applies the constraint to the axes.
			/// </summary>
			/// <param name="pXAxis1">The bottom x-axis.</param>
			/// <param name="pYAxis1">The left y-axis.</param>
			/// <param name="pXAxis2">The top x-axis.</param>
			/// <param name="pYAxis2">The right y-axis.</param>
			public override void ApplyConstraint( 
				PhysicalAxis pXAxis1, PhysicalAxis pYAxis1, 
				PhysicalAxis pXAxis2, PhysicalAxis pYAxis2 )
			{
				int desiredLength = (int)(pXAxis1.Axis.WorldLength / (double)this.pWorldLength_);
				int currentLength = pXAxis1.PhysicalLength;
				int delta = currentLength - desiredLength;

				int changeLeft = delta / 2;
				int changeRight = delta / 2;
				if (this.holdFixedY_ != null)
				{
					if ( (PlotSurface2D.YAxisPosition)this.holdFixedY_ == PlotSurface2D.YAxisPosition.Left )
					{
						changeLeft = 0;
						changeRight = delta;
					}
					else
					{
						changeLeft = delta;
						changeRight = 0;
					}
				}

				pXAxis1.PhysicalMin = new Point( pXAxis1.PhysicalMin.X+changeLeft, pXAxis1.PhysicalMin.Y );
				pXAxis1.PhysicalMax = new Point( pXAxis1.PhysicalMax.X-changeRight, pXAxis1.PhysicalMax.Y );
				pXAxis2.PhysicalMin = new Point( pXAxis2.PhysicalMin.X+changeLeft, pXAxis2.PhysicalMin.Y );
				pXAxis2.PhysicalMax = new Point( pXAxis2.PhysicalMax.X-changeRight, pXAxis2.PhysicalMax.Y );

				pYAxis1.PhysicalMin = new Point( pYAxis1.PhysicalMin.X+changeLeft, pYAxis1.PhysicalMin.Y );
				pYAxis1.PhysicalMax = new Point( pYAxis1.PhysicalMax.X+changeLeft, pYAxis1.PhysicalMax.Y );
				pYAxis2.PhysicalMin = new Point( pYAxis2.PhysicalMin.X-changeRight, pYAxis2.PhysicalMin.Y );
				pYAxis2.PhysicalMax = new Point( pYAxis2.PhysicalMax.X-changeRight, pYAxis2.PhysicalMax.Y );
		
			}
		}


		/// <summary>
		/// Defines an AxisConstraint that forces the world length corresponding
		/// to one pixel on the left y-axis to be a certain value. 
		/// 
		/// TODO: Allow the pixel world length to be set for the right axis.
		/// </summary>
		public class YPixelWorldLength : AxesConstraint
		{
			private double pWorldLength_ = 0.0;
			private object holdFixedX_ = null;

			/// <summary>
			/// Constructor, which defines the world pixel length only. Both 
			/// x-axes will be moved by equal amounts in order to force this
			/// constraint.
			/// </summary>
			/// <param name="p">The world pixel length</param>
			public YPixelWorldLength( double p )
			{
				this.pWorldLength_ = p;
			}
		
			/// <summary>
			/// Constructor, which defines the world pixel length together with
			/// the x-axis that should be held constant when forcing this 
			/// constraint [the other x-axis only will be moved].
			/// </summary>
			/// <param name="p">The world pixel length</param>
			/// <param name="holdFixedX">The position of this x-axis will be held constant. The other x-axis will be moved in order to force the constraint.</param>
			public YPixelWorldLength( double p, PlotSurface2D.XAxisPosition holdFixedX )
			{
				this.pWorldLength_ = p;
				this.holdFixedX_ = holdFixedX;
			}


			/// <summary>
			/// Applies the constraint to the axes.
			/// </summary>
			/// <param name="pXAxis1">The bottom x-axis.</param>
			/// <param name="pYAxis1">The left y-axis.</param>
			/// <param name="pXAxis2">The top x-axis.</param>
			/// <param name="pYAxis2">The right y-axis.</param>
			public override void ApplyConstraint( 
				PhysicalAxis pXAxis1, PhysicalAxis pYAxis1, 
				PhysicalAxis pXAxis2, PhysicalAxis pYAxis2 )
			{

				int desiredLength = (int)(pYAxis1.Axis.WorldLength / this.pWorldLength_);
				int currentLength = pYAxis1.PhysicalLength;
				int delta = currentLength - desiredLength;

				int changeBottom = -delta / 2;
				int changeTop = -delta / 2;
				if (this.holdFixedX_ != null)
				{
					if ( (PlotSurface2D.XAxisPosition)this.holdFixedX_ == PlotSurface2D.XAxisPosition.Bottom )
					{
						changeBottom = 0;
						changeTop = -delta;
					}
					else
					{
						changeBottom = -delta;
						changeTop = 0;
					}
				}

				pYAxis1.PhysicalMin = new Point( pYAxis1.PhysicalMin.X, pYAxis1.PhysicalMin.Y+changeBottom );
				pYAxis1.PhysicalMax = new Point( pYAxis1.PhysicalMax.X, pYAxis1.PhysicalMax.Y-changeTop );
				pYAxis2.PhysicalMin = new Point( pYAxis2.PhysicalMin.X, pYAxis2.PhysicalMin.Y+changeBottom );
				pYAxis2.PhysicalMax = new Point( pYAxis2.PhysicalMax.X, pYAxis2.PhysicalMax.Y-changeTop );

				pXAxis1.PhysicalMin = new Point( pXAxis1.PhysicalMin.X, pXAxis1.PhysicalMin.Y+changeBottom );
				pXAxis1.PhysicalMax = new Point( pXAxis1.PhysicalMax.X, pXAxis1.PhysicalMax.Y+changeBottom );
				pXAxis2.PhysicalMin = new Point( pXAxis2.PhysicalMin.X, pXAxis2.PhysicalMin.Y-changeTop );
				pXAxis2.PhysicalMax = new Point( pXAxis2.PhysicalMax.X, pXAxis2.PhysicalMax.Y-changeTop );

			}
		}


		/// <summary>
		/// Defines an AxisConstraint that forces the specified axis to be placed at a 
		/// specific physical position. The position of the axis opposite is held 
		/// constant.
		/// </summary>
		public class AxisPosition : AxesConstraint
		{

			private object xAxisPosition_;
			private object yAxisPosition_;
			private int position_;

			
			/// <summary>
			/// Constructor, which defines an horizontal axis and the physical
			/// y position it should be drawn at.
			/// </summary>
			/// <param name="axis">The x-axis for which the y position is to be specified.</param>
			/// <param name="yPosition">The [physical] y position of the axis.</param>
			public AxisPosition( PlotSurface2D.XAxisPosition axis, int yPosition )
			{
				position_ = yPosition;
				xAxisPosition_ = axis;
			}


			/// <summary>
			/// Constructor, which defines a vertical axis and the physical
			/// x position it should be drawn at.
			/// </summary>
			/// <param name="axis">The y-axis for which the x position is to be specified.</param>
			/// <param name="xPosition">The [physical] x position of the axis.</param>
			public AxisPosition( PlotSurface2D.YAxisPosition axis, int xPosition )
			{
				position_ = xPosition;
				yAxisPosition_ = axis;
			}

			/// <summary>
			/// Applies the constraint to the axes.
			/// </summary>
			/// <param name="pXAxis1">The bottom x-axis.</param>
			/// <param name="pYAxis1">The left y-axis.</param>
			/// <param name="pXAxis2">The top x-axis.</param>
			/// <param name="pYAxis2">The right y-axis.</param>
			public override void ApplyConstraint( 
				PhysicalAxis pXAxis1, PhysicalAxis pYAxis1, 
				PhysicalAxis pXAxis2, PhysicalAxis pYAxis2 )
			{

				if ( xAxisPosition_ != null )
				{

					if ((PlotSurface2D.XAxisPosition)xAxisPosition_ == PlotSurface2D.XAxisPosition.Bottom)
					{
						pXAxis1.PhysicalMin = new Point( pXAxis1.PhysicalMin.X, position_ );
						pXAxis1.PhysicalMax = new Point( pXAxis1.PhysicalMax.X, position_ );
			
						pYAxis1.PhysicalMin = new Point( pYAxis1.PhysicalMin.X, position_ );
						pYAxis2.PhysicalMin = new Point( pYAxis2.PhysicalMin.X, position_ );
					}
					else
					{
						pXAxis2.PhysicalMin = new Point( pXAxis2.PhysicalMin.X, position_ );
						pXAxis2.PhysicalMax = new Point( pXAxis2.PhysicalMax.X, position_ );

						pYAxis1.PhysicalMax = new Point( pYAxis1.PhysicalMax.X, position_ );
						pYAxis2.PhysicalMax = new Point( pYAxis2.PhysicalMax.X, position_ );
					}

				}
				else if (yAxisPosition_ != null )
				{

					if ((PlotSurface2D.YAxisPosition)yAxisPosition_ == PlotSurface2D.YAxisPosition.Left)
					{
						pYAxis1.PhysicalMin = new Point( position_, pYAxis1.PhysicalMin.Y );
						pYAxis1.PhysicalMax = new Point( position_, pYAxis1.PhysicalMax.Y );

						pXAxis1.PhysicalMin = new Point( position_, pXAxis1.PhysicalMin.Y );
						pXAxis2.PhysicalMin = new Point( position_, pXAxis2.PhysicalMin.Y );
					}
					else
					{
						pYAxis2.PhysicalMin = new Point( position_, pYAxis2.PhysicalMin.Y );
						pYAxis2.PhysicalMax = new Point( position_, pYAxis2.PhysicalMax.Y );
				
						pXAxis1.PhysicalMax = new Point( position_, pXAxis1.PhysicalMax.Y );
						pXAxis2.PhysicalMax = new Point( position_, pXAxis2.PhysicalMax.Y );
					}

				}

			}

		}


		/// <summary>
		/// Defines an axes constraint that forces the world width and height pixel lengths
		/// to be at the provided ratio. For example, an aspect ratio of 3:2 or
		/// 1.5 indicates that there should be 1.5 times as many pixels per fixed
		/// world length along the x direction than for the same world length along
		/// the y direction. In other words, the world length of one pixel along 
		/// the x direction is 2/3rds that of the world length of one pixel height
		/// in the y direction.
		/// </summary>
		/// <remarks>
		/// This class will never increase the size of the plot bounding box. It 
		/// will always be made smaller.
		/// </remarks>
		public class AspectRatio : AxesConstraint
		{
			private double a_;
			private object holdFixedX_ = null;
			private object holdFixedY_ = null;

			/// <summary>
			/// Constructor.
			/// </summary>
			/// <param name="a">Aspect Ratio</param>
			public AspectRatio( double a )
			{
				this.a_ = a;
			}

			/// <summary>
			/// Constructor
			/// </summary>
			/// <param name="a">Aspect Ratio</param>
			/// <param name="holdFixedX">
			/// When adjusting the position of axes, the specified axis will never
			/// be moved.
			/// </param>
			public AspectRatio( double a, PlotSurface2D.XAxisPosition holdFixedX )
			{
				this.a_ = a;
				this.holdFixedX_ = holdFixedX;
			}

			/// <summary>
			/// Constructor
			/// </summary>
			/// <param name="a">Aspect Ratio</param>
			/// <param name="holdFixedY">
			/// When adjusting the position of axes, the 
			/// specified axis will never be moved.
			/// </param>
			public AspectRatio( double a, PlotSurface2D.YAxisPosition holdFixedY )
			{
				this.a_ = a;
				this.holdFixedY_ = holdFixedY;
			}

			/// <summary>
			/// Constructor
			/// </summary>
			/// <param name="a">Aspect Ratio</param>
			/// <param name="holdFixedX">When adjusting the position of axes, the specified axis will never be moved.</param>
			/// <param name="holdFixedY">When adjusting the position of axes, the specified axis will never be moved.</param>
			public AspectRatio( 
				double a,
				PlotSurface2D.XAxisPosition holdFixedX, 
				PlotSurface2D.YAxisPosition holdFixedY )
			{
				this.a_ = a;
				this.holdFixedX_ = holdFixedX;
				this.holdFixedY_ = holdFixedY;
			}
							
			/// <summary>
			/// Applies the constraint to the axes.
			/// </summary>
			/// <param name="pXAxis1">The bottom x-axis.</param>
			/// <param name="pYAxis1">The left y-axis.</param>
			/// <param name="pXAxis2">The top x-axis.</param>
			/// <param name="pYAxis2">The right y-axis.</param>
			public override void ApplyConstraint( 
				PhysicalAxis pXAxis1, PhysicalAxis pYAxis1, 
				PhysicalAxis pXAxis2, PhysicalAxis pYAxis2 )
			{
				double xWorldRange = Math.Abs( pXAxis1.Axis.WorldMax - pXAxis1.Axis.WorldMin );
				double xPhysicalRange = Math.Abs( pXAxis1.PhysicalMax.X - pXAxis1.PhysicalMin.X );
				double xDirPixelSize =  xWorldRange / xPhysicalRange;
			
				double yWorldRange = Math.Abs( pYAxis1.Axis.WorldMax - pYAxis1.Axis.WorldMin );
				double yPhysicalRange = Math.Abs( pYAxis1.PhysicalMax.Y - pYAxis1.PhysicalMin.Y );
				double yDirPixelSize =  yWorldRange / yPhysicalRange;

				double currentAspectRatio = yDirPixelSize / xDirPixelSize;

				// we want to change the current aspect ratio to be the desired.
				// to do this, we may only add the world pixel lengths.

				if ( this.a_ > currentAspectRatio )
				{
					// want to increase aspect ratio. Therefore, want to add some amount
					// to yDirPixelSize (numerator).

					double toAdd = ( this.a_ - currentAspectRatio ) * xDirPixelSize;
					int newHeight =
						(int)( Math.Abs(pYAxis1.Axis.WorldMax - pYAxis1.Axis.WorldMin) / (yDirPixelSize + toAdd) );
					int changeInHeight = (int)yPhysicalRange - newHeight;

					int changeBottom = changeInHeight/2;
					int changeTop = changeInHeight/2;
					if (this.holdFixedX_ != null)
					{
						if ( (PlotSurface2D.XAxisPosition)this.holdFixedX_ == PlotSurface2D.XAxisPosition.Bottom )
						{
							changeBottom = 0;
							changeTop = changeInHeight;
						}
						else
						{
							changeBottom = changeInHeight;
							changeTop = 0;
						}
					}

					pYAxis1.PhysicalMin = new Point( pYAxis1.PhysicalMin.X, pYAxis1.PhysicalMin.Y-changeBottom );
					pYAxis1.PhysicalMax = new Point( pYAxis1.PhysicalMax.X, pYAxis1.PhysicalMax.Y+changeTop );
					pYAxis2.PhysicalMin = new Point( pYAxis2.PhysicalMin.X, pYAxis2.PhysicalMin.Y-changeBottom );
					pYAxis2.PhysicalMax = new Point( pYAxis2.PhysicalMax.X, pYAxis2.PhysicalMax.Y+changeTop );

					pXAxis1.PhysicalMin = new Point( pXAxis1.PhysicalMin.X, pXAxis1.PhysicalMin.Y-changeBottom );
					pXAxis1.PhysicalMax = new Point( pXAxis1.PhysicalMax.X, pXAxis1.PhysicalMax.Y-changeBottom );
					pXAxis2.PhysicalMin = new Point( pXAxis2.PhysicalMin.X, pXAxis2.PhysicalMin.Y+changeTop );
					pXAxis2.PhysicalMax = new Point( pXAxis2.PhysicalMax.X, pXAxis2.PhysicalMax.Y+changeTop );

				}

				else 
				{

					// want to decrease aspect ratio. Therefore, want to add some amount
					// to xDirPixelSize (denominator).

					double toAdd = yDirPixelSize / this.a_ - xDirPixelSize;
					int newWidth = 
						(int)( Math.Abs(pXAxis1.Axis.WorldMax - pXAxis1.Axis.WorldMin) / (xDirPixelSize + toAdd) );
					int changeInWidth = (int)xPhysicalRange - newWidth;

					int changeLeft = changeInWidth / 2;
					int changeRight = changeInWidth / 2;
					if (this.holdFixedY_ != null)
					{
						if ( (PlotSurface2D.YAxisPosition)this.holdFixedY_ == PlotSurface2D.YAxisPosition.Left )
						{
							changeLeft = 0;
							changeRight = changeInWidth;
						}
						else
						{
							changeLeft = changeInWidth;
							changeRight = 0;
						}
					}

					pXAxis1.PhysicalMin = new Point( pXAxis1.PhysicalMin.X+changeLeft, pXAxis1.PhysicalMin.Y );
					pXAxis1.PhysicalMax = new Point( pXAxis1.PhysicalMax.X-changeRight, pXAxis1.PhysicalMax.Y );
					pXAxis2.PhysicalMin = new Point( pXAxis2.PhysicalMin.X+changeLeft, pXAxis2.PhysicalMin.Y );
					pXAxis2.PhysicalMax = new Point( pXAxis2.PhysicalMax.X-changeRight, pXAxis2.PhysicalMax.Y );

					pYAxis1.PhysicalMin = new Point( pYAxis1.PhysicalMin.X+changeLeft, pYAxis1.PhysicalMin.Y );
					pYAxis1.PhysicalMax = new Point( pYAxis1.PhysicalMax.X+changeLeft, pYAxis1.PhysicalMax.Y );
					pYAxis2.PhysicalMin = new Point( pYAxis2.PhysicalMin.X-changeRight, pYAxis2.PhysicalMin.Y );
					pYAxis2.PhysicalMax = new Point( pYAxis2.PhysicalMax.X-changeRight, pYAxis2.PhysicalMax.Y );

				}

			}

		}
		

		/// <summary>
		/// Applies the constraint to the axes. Must be overriden.
		/// </summary>
		/// <param name="pXAxis1">The bottom x-axis.</param>
		/// <param name="pYAxis1">The left y-axis.</param>
		/// <param name="pXAxis2">The top x-axis.</param>
		/// <param name="pYAxis2">The right y-axis.</param>
		public abstract void ApplyConstraint( 
			PhysicalAxis pXAxis1, PhysicalAxis pYAxis1, 
			PhysicalAxis pXAxis2, PhysicalAxis pYAxis2 );
	}

}
