/*
 * 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;

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>
        /// 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);

        /// <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 readonly double a_;
            private readonly object holdFixedX_;
            private readonly object holdFixedY_;

            /// <summary>
            /// Constructor.
            /// </summary>
            /// <param name="a">Aspect Ratio</param>
            public AspectRatio(double a)
            {
                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)
            {
                a_ = a;
                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)
            {
                a_ = a;
                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)
            {
                a_ = a;
                holdFixedX_ = holdFixedX;
                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 (a_ > currentAspectRatio)
                {
                    // want to increase aspect ratio. Therefore, want to add some amount
                    // to yDirPixelSize (numerator).

                    double toAdd = (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 (holdFixedX_ != null)
                    {
                        if ((PlotSurface2D.XAxisPosition) 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/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 (holdFixedY_ != null)
                    {
                        if ((PlotSurface2D.YAxisPosition) 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>
        /// 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 readonly int position_;
            private readonly object xAxisPosition_;
            private readonly object yAxisPosition_;

            /// <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 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 readonly object holdFixedY_;
            private readonly double pWorldLength_;

            /// <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)
            {
                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)
            {
                pWorldLength_ = p;
                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/pWorldLength_);
                int currentLength = pXAxis1.PhysicalLength;
                int delta = currentLength - desiredLength;

                int changeLeft = delta/2;
                int changeRight = delta/2;
                if (holdFixedY_ != null)
                {
                    if ((PlotSurface2D.YAxisPosition) 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 readonly object holdFixedX_;
            private readonly double pWorldLength_;

            /// <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)
            {
                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)
            {
                pWorldLength_ = p;
                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/pWorldLength_);
                int currentLength = pYAxis1.PhysicalLength;
                int delta = currentLength - desiredLength;

                int changeBottom = -delta/2;
                int changeTop = -delta/2;
                if (holdFixedX_ != null)
                {
                    if ((PlotSurface2D.XAxisPosition) 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);
            }
        }
    }
}