/* Implementation of the degree trignometric functions COSD, SIND, TAND.
   Copyright (C) 2020 Free Software Foundation, Inc.
   Contributed by Steven G. Kargl <kargl@gcc.gnu.org>
   and Fritz Reese <foreese@gcc.gnu.org>

This file is part of the GNU Fortran runtime library (libgfortran).

Libgfortran 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.

Libgfortran 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.

Under Section 7 of GPL version 3, you are granted additional
permissions described in the GCC Runtime Library Exception, version
3.1, as published by the Free Software Foundation.

You should have received a copy of the GNU General Public License and
a copy of the GCC Runtime Library Exception along with this program;
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
<http://www.gnu.org/licenses/>.  */


/*

This file is included from both the FE and the runtime library code.
Operations are generalized using GMP/MPFR functions. When included from
libgfortran, these should be overridden using macros which will use native
operations conforming to the same API. From the FE, the GMP/MPFR functions can
be used as-is.

The following macros are used and must be defined, unless listed as [optional]:

FTYPE
    Type name for the real-valued parameter.
    Variables of this type are constructed/destroyed using mpfr_init()
    and mpfr_clear.

RETTYPE
    Return type of the functions.

RETURN(x)
    Insert code to return a value.
    The parameter x is the result variable, which was also the input parameter.

ITYPE
    Type name for integer types.

SIND, COSD, TRIGD
    Names for the degree-valued trig functions defined by this module.

ENABLE_SIND, ENABLE_COSD, ENABLE_TAND
    Whether the degree-valued trig functions can be enabled.

ERROR_RETURN(f, k, x)
    If ENABLE_<xxx>D is not defined, this is substituted to assert an
    error condition for function f, kind k, and parameter x.
    The function argument is one of {sind, cosd, tand}.

ISFINITE(x)
    Whether x is a regular number or zero (not inf or NaN).

D2R(x)
    Convert x from radians to degrees.

SET_COSD30(x)
    Set x to COSD(30), or equivalently, SIND(60).

TINY_LITERAL [optional]
    Value subtracted from 1 to cause raise INEXACT for COSD(x) for x << 1.
    If not set, COSD(x) for x <= COSD_SMALL_LITERAL simply returns 1.

COSD_SMALL_LITERAL [optional]
    Value such that x <= COSD_SMALL_LITERAL implies COSD(x) = 1 to within the
    precision of FTYPE. If not set, this condition is not checked.

SIND_SMALL_LITERAL [optional]
    Value such that x <= SIND_SMALL_LITERAL implies SIND(x) = D2R(x) to within
    the precision of FTYPE. If not set, this condition is not checked.

*/


#ifdef SIND
/* Compute sind(x) = sin(x * pi / 180). */

RETTYPE
SIND (FTYPE x)
{
#ifdef ENABLE_SIND
  if (ISFINITE (x))
    {
      FTYPE s, one;

      /* sin(-x) = - sin(x).  */
      mpfr_init (s);
      mpfr_init_set_ui (one, 1, GFC_RND_MODE);
      mpfr_copysign (s, one, x, GFC_RND_MODE);
      mpfr_clear (one);

#ifdef SIND_SMALL_LITERAL
      /* sin(x) = x as x -> 0; but only for some precisions. */
      FTYPE ax;
      mpfr_init (ax);
      mpfr_abs (ax, x, GFC_RND_MODE);
      if (mpfr_cmp_ld (ax, SIND_SMALL_LITERAL) < 0)
	{
	  D2R (x);
	  mpfr_clear (ax);
	  return x;
	}

      mpfr_swap (x, ax);
      mpfr_clear (ax);

#else
      mpfr_abs (x, x, GFC_RND_MODE);
#endif /* SIND_SMALL_LITERAL */

      /* Reduce angle to x in [0,360].  */
      FTYPE period;
      mpfr_init_set_ui (period, 360, GFC_RND_MODE);
      mpfr_fmod (x, x, period, GFC_RND_MODE);
      mpfr_clear (period);

      /* Special cases with exact results.  */
      ITYPE n;
      mpz_init (n);
      if (mpfr_get_z (n, x, GFC_RND_MODE) == 0 && mpz_divisible_ui_p (n, 30))
	{
	  /* Flip sign for odd n*pi (x is % 360 so this is only for 180).
	     This respects sgn(sin(x)) = sgn(d/dx sin(x)) = sgn(cos(x)). */
	  if (mpz_divisible_ui_p (n, 180))
	    {
	      mpfr_set_ui (x, 0, GFC_RND_MODE);
	      if (mpz_cmp_ui (n, 180) == 0)
		mpfr_neg (s, s, GFC_RND_MODE);
	    }
	  else if (mpz_divisible_ui_p (n, 90))
	    mpfr_set_si (x, (mpz_cmp_ui (n, 90) == 0 ? 1 : -1), GFC_RND_MODE);
	  else if (mpz_divisible_ui_p (n, 60))
	    {
	      SET_COSD30 (x);
	      if (mpz_cmp_ui (n, 180) >= 0)
		mpfr_neg (x, x, GFC_RND_MODE);
	    }
	  else
	    mpfr_set_ld (x, (mpz_cmp_ui (n, 180) < 0 ? 0.5L : -0.5L),
			 GFC_RND_MODE);
	}

      /* Fold [0,360] into the range [0,45], and compute either SIN() or
         COS() depending on symmetry of shifting into the [0,45] range.  */
      else
	{
	  bool fold_cos = false;
	  if (mpfr_cmp_ui (x, 180) <= 0)
	    {
	      if (mpfr_cmp_ui (x, 90) <= 0)
		{
		  if (mpfr_cmp_ui (x, 45) > 0)
		    {
		      /* x = COS(D2R(90 - x)) */
		      mpfr_ui_sub (x, 90, x, GFC_RND_MODE);
		      fold_cos = true;
		    }
		}
	      else
		{
		  if (mpfr_cmp_ui (x, 135) <= 0)
		    {
		      mpfr_sub_ui (x, x, 90, GFC_RND_MODE);
		      fold_cos = true;
		    }
		  else
		    mpfr_ui_sub (x, 180, x, GFC_RND_MODE);
		}
	    }

	  else if (mpfr_cmp_ui (x, 270) <= 0)
	    {
	      if (mpfr_cmp_ui (x, 225) <= 0)
		mpfr_sub_ui (x, x, 180, GFC_RND_MODE);
	      else
		{
		  mpfr_ui_sub (x, 270, x, GFC_RND_MODE);
		  fold_cos = true;
		}
	      mpfr_neg (s, s, GFC_RND_MODE);
	    }

	  else
	    {
	      if (mpfr_cmp_ui (x, 315) <= 0)
		{
		  mpfr_sub_ui (x, x, 270, GFC_RND_MODE);
		  fold_cos = true;
		}
	      else
		mpfr_ui_sub (x, 360, x, GFC_RND_MODE);
	      mpfr_neg (s, s, GFC_RND_MODE);
	    }

	  D2R (x);

	  if (fold_cos)
	    mpfr_cos (x, x, GFC_RND_MODE);
	  else
	    mpfr_sin (x, x, GFC_RND_MODE);
	}

      mpfr_mul (x, x, s, GFC_RND_MODE);
      mpz_clear (n);
      mpfr_clear (s);
    }

  /* Return NaN for +-Inf and NaN and raise exception.  */
  else
    mpfr_sub (x, x, x, GFC_RND_MODE);

  RETURN (x);

#else
  ERROR_RETURN(sind, KIND, x);
#endif // ENABLE_SIND
}
#endif // SIND


#ifdef COSD
/* Compute cosd(x) = cos(x * pi / 180).  */

RETTYPE
COSD (FTYPE x)
{
#ifdef ENABLE_COSD
#if defined(TINY_LITERAL) && defined(COSD_SMALL_LITERAL)
  static const volatile FTYPE tiny = TINY_LITERAL;
#endif

  if (ISFINITE (x))
    {
#ifdef COSD_SMALL_LITERAL
      FTYPE ax;
      mpfr_init (ax);

      mpfr_abs (ax, x, GFC_RND_MODE);
      /* No spurious underflows!.  In radians, cos(x) = 1-x*x/2 as x -> 0.  */
      if (mpfr_cmp_ld (ax, COSD_SMALL_LITERAL) <= 0)
	{
	  mpfr_set_ui (x, 1, GFC_RND_MODE);
#ifdef TINY_LITERAL
	  /* Cause INEXACT.  */
	  if (!mpfr_zero_p (ax))
	    mpfr_sub_d (x, x, tiny, GFC_RND_MODE);
#endif

	  mpfr_clear (ax);
	  return x;
	}

      mpfr_swap (x, ax);
      mpfr_clear (ax);
#else
      mpfr_abs (x, x, GFC_RND_MODE);
#endif /* COSD_SMALL_LITERAL */

      /* Reduce angle to ax in [0,360].  */
      FTYPE period;
      mpfr_init_set_ui (period, 360, GFC_RND_MODE);
      mpfr_fmod (x, x, period, GFC_RND_MODE);
      mpfr_clear (period);

      /* Special cases with exact results.
         Return negative zero for cosd(270) for consistency with libm cos().  */
      ITYPE n;
      mpz_init (n);
      if (mpfr_get_z (n, x, GFC_RND_MODE) == 0 && mpz_divisible_ui_p (n, 30))
	{
	  if (mpz_divisible_ui_p (n, 180))
	    mpfr_set_si (x, (mpz_cmp_ui (n, 180) == 0 ? -1 : 1),
			 GFC_RND_MODE);
	  else if (mpz_divisible_ui_p (n, 90))
	    mpfr_set_zero (x, 0);
	  else if (mpz_divisible_ui_p (n, 60))
	    {
	      mpfr_set_ld (x, 0.5, GFC_RND_MODE);
	      if (mpz_cmp_ui (n, 60) != 0 && mpz_cmp_ui (n, 300) != 0)
		mpfr_neg (x, x, GFC_RND_MODE);
	    }
	  else
	    {
	      SET_COSD30 (x);
	      if (mpz_cmp_ui (n, 30) != 0 && mpz_cmp_ui (n, 330) != 0)
		mpfr_neg (x, x, GFC_RND_MODE);
	    }
	}

      /* Fold [0,360] into the range [0,45], and compute either SIN() or
         COS() depending on symmetry of shifting into the [0,45] range.  */
      else
	{
	  bool neg = false;
	  bool fold_sin = false;
	  if (mpfr_cmp_ui (x, 180) <= 0)
	    {
	      if (mpfr_cmp_ui (x, 90) <= 0)
		{
		  if (mpfr_cmp_ui (x, 45) > 0)
		    {
		      mpfr_ui_sub (x, 90, x, GFC_RND_MODE);
		      fold_sin = true;
		    }
		}
	      else
		{
		  if (mpfr_cmp_ui (x, 135) <= 0)
		    {
		      mpfr_sub_ui (x, x, 90, GFC_RND_MODE);
		      fold_sin = true;
		    }
		  else
		    mpfr_ui_sub (x, 180, x, GFC_RND_MODE);
		  neg = true;
		}
	    }

	  else if (mpfr_cmp_ui (x, 270) <= 0)
	    {
	      if (mpfr_cmp_ui (x, 225) <= 0)
		mpfr_sub_ui (x, x, 180, GFC_RND_MODE);
	      else
		{
		  mpfr_ui_sub (x, 270, x, GFC_RND_MODE);
		  fold_sin = true;
		}
	      neg = true;
	    }

	  else
	    {
	      if (mpfr_cmp_ui (x, 315) <= 0)
		{
		  mpfr_sub_ui (x, x, 270, GFC_RND_MODE);
		  fold_sin = true;
		}
	      else
		mpfr_ui_sub (x, 360, x, GFC_RND_MODE);
	    }

	  D2R (x);

	  if (fold_sin)
	    mpfr_sin (x, x, GFC_RND_MODE);
	  else
	    mpfr_cos (x, x, GFC_RND_MODE);

	  if (neg)
	    mpfr_neg (x, x, GFC_RND_MODE);
	}

      mpz_clear (n);
    }

  /* Return NaN for +-Inf and NaN and raise exception.  */
  else
    mpfr_sub (x, x, x, GFC_RND_MODE);

  RETURN (x);

#else
  ERROR_RETURN(cosd, KIND, x);
#endif // ENABLE_COSD
}
#endif // COSD


#ifdef TAND
/* Compute tand(x) = tan(x * pi / 180).  */

RETTYPE
TAND (FTYPE x)
{
#ifdef ENABLE_TAND
  if (ISFINITE (x))
    {
      FTYPE s, one;

      /* tan(-x) = - tan(x).  */
      mpfr_init (s);
      mpfr_init_set_ui (one, 1, GFC_RND_MODE);
      mpfr_copysign (s, one, x, GFC_RND_MODE);
      mpfr_clear (one);

#ifdef SIND_SMALL_LITERAL
      /* tan(x) = x as x -> 0; but only for some precisions. */
      FTYPE ax;
      mpfr_init (ax);
      mpfr_abs (ax, x, GFC_RND_MODE);
      if (mpfr_cmp_ld (ax, SIND_SMALL_LITERAL) < 0)
	{
	  D2R (x);
	  mpfr_clear (ax);
	  return x;
	}

      mpfr_swap (x, ax);
      mpfr_clear (ax);

#else
      mpfr_abs (x, x, GFC_RND_MODE);
#endif /* SIND_SMALL_LITERAL */

      /* Reduce angle to x in [0,360].  */
      FTYPE period;
      mpfr_init_set_ui (period, 360, GFC_RND_MODE);
      mpfr_fmod (x, x, period, GFC_RND_MODE);
      mpfr_clear (period);

      /* Special cases with exact results. */
      ITYPE n;
      mpz_init (n);
      if (mpfr_get_z (n, x, GFC_RND_MODE) == 0 && mpz_divisible_ui_p (n, 45))
	{
	  if (mpz_divisible_ui_p (n, 180))
	    mpfr_set_zero (x, 0);

	  /* Though mathematically NaN is more appropriate for tan(n*90),
	     returning +/-Inf offers the advantage that 1/tan(n*90) returns 0,
	     which is mathematically sound. In fact we rely on this behavior
	     to implement COTAND(x) = 1 / TAND(x).
	   */
	  else if (mpz_divisible_ui_p (n, 90))
	    mpfr_set_inf (x, mpz_cmp_ui (n, 90) == 0 ? 0 : 1);

	  else
	    {
	      mpfr_set_ui (x, 1, GFC_RND_MODE);
	      if (mpz_cmp_ui (n, 45) != 0 && mpz_cmp_ui (n, 225) != 0)
		mpfr_neg (x, x, GFC_RND_MODE);
	    }
	}

      else
	{
	  /* Fold [0,360] into the range [0,90], and compute TAN().  */
	  if (mpfr_cmp_ui (x, 180) <= 0)
	    {
	      if (mpfr_cmp_ui (x, 90) > 0)
		{
		  mpfr_ui_sub (x, 180, x, GFC_RND_MODE);
		  mpfr_neg (s, s, GFC_RND_MODE);
		}
	    }
	  else
	    {
	      if (mpfr_cmp_ui (x, 270) <= 0)
		{
		  mpfr_sub_ui (x, x, 180, GFC_RND_MODE);
		}
	      else
		{
		  mpfr_ui_sub (x, 360, x, GFC_RND_MODE);
		  mpfr_neg (s, s, GFC_RND_MODE);
		}
	    }

	  D2R (x);
	  mpfr_tan (x, x, GFC_RND_MODE);
	}

      mpfr_mul (x, x, s, GFC_RND_MODE);
      mpz_clear (n);
      mpfr_clear (s);
    }

  /* Return NaN for +-Inf and NaN and raise exception.  */
  else
    mpfr_sub (x, x, x, GFC_RND_MODE);

  RETURN (x);

#else
  ERROR_RETURN(tand, KIND, x);
#endif // ENABLE_TAND
}
#endif // TAND

/* vim: set ft=c: */
