// Copyright (c) 2015, Pantor Engineering AB
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//  * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//
//  * 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.
//
//  * Neither the name of Pantor Engineering AB 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 HOLDERS 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.

package com.pantor.blink;

import java.math.BigDecimal;

/**
   The {@code FixedDec} represents a Blink fixedDec(N) value. It has
   an 64-bit significand and a fixed scale.

   <p>Instances of {@code FixedDec} are immutable.</p>
*/

public abstract class FixedDec implements Comparable<FixedDec>
{   
   /**
      Creates a decimal with the value {@code significand} * 10^-N

      @param significand a significand
   */

   FixedDec (long significand) { this.significand = significand; }

   /**
      Returns the scale

      @return the scale
   */

   public abstract int getScale ();
   
   /**
      Returns the significand

      @return the significand
   */

   public long getSignificand ()
   {
      return significand;
   }

   /**
      Compares this fixed decimal with the specified object for
      equality. Two fixed decimals are considered equal only if the
      have the same scale and the same significand.

      @param other object to compare with

      @return true if and only if both the significands and the scales
      are equal
   */
   
   @Override
   public final boolean equals (Object other)
   {
      if (other instanceof FixedDec)
      {
         FixedDec o = (FixedDec)other;
         return significand == o.significand && getScale () == o.getScale ();
      }
      else
         return false;
   }

   /**
      Returns a hash for this fixed decimal. Two fixed decimals with different
      scales but that are numerically equal can have different hash codes

      @return hash code for this fixed decimal
   */

   @Override
   public final int hashCode ()
   {
      return (int)significand * 31 + getScale ();
   }

   private final static int compareSigs (long s1, long s2)
   {
      return s1 == s2 ? 0 : (s1 < s2 ? -1 : 1);
   }
   
   @Override
   public final int compareTo (FixedDec other)
   {
      int scale = getScale ();
      int otherScale = other.getScale ();
      long otherSig = other.getSignificand ();

      if (scale == otherScale)
         return compareSigs (significand, otherSig);
      else if (significand == 0 && otherSig == 0)
         return 0;
      else if (significand < 0 && otherSig >= 0)
         return -1;
      else if (significand >= 0 && otherSig < 0)
         return 1;
      else if (scale < otherScale)
         // FIXME: Deal with precision loss
         return compareSigs (rescale (significand, scale, otherScale), otherSig);
      else
         // FIXME: Deal with precision loss
         return compareSigs (significand, rescale (otherSig, otherScale, scale));
   }
   
   @Override
   public final String toString ()
   {
      int scale = getScale ();
      if (scale == 0)
         return String.valueOf (significand) + "E0";
      else
         return String.valueOf (significand) + "E-" + String.valueOf (scale);
   }

   /**
      Returns a double representation of this value

      @return the double value of this fixed decimal
   */

   public final double doubleValue ()
   {
      return (double)significand * Math.pow (10, - getScale ()); 
   }

   /**
      Returns a long representation of this value

      @return the long value of this fixed decimal
   */

   public final long longValue ()
   {
      return rescale (significand, getScale (), 0);
   }

   /**
      Returns a decimal representation of this value

      @return the decimal value of this fixed decimal
   */

   public final Decimal decimalValue ()
   {
      return Decimal.valueOf (significand, - getScale ());
   }

   /**
      Creates a fixedDec (0) instance from an integer

      @param value an long integer
      @return a fixed decimal value
   */

   public static FixedDec valueOf (long value)
   {
      return new _0 (value);
   }

   /**
      Creates a fixedDec({@code scale}) instance from a scaled integer

      @param value an long integer
      @param fromScale the scale of the input long integer
      @param toScale the scale of the returned fixed decimal, the scale
      must be >= 0
      @return a fixed decimal value
   */

   public static FixedDec valueOf (long value, int fromScale, int toScale)
   {
      return getInstance (rescale (value, fromScale, toScale), toScale);
   }

   /**
      Creates a fixedDec({@code scale}) instance from significand and
      a scale

      @param significand a significand
      @param scale the scale which must be >= 0
      @return a fixed decimal value
   */
   
   public static FixedDec getInstance (long significand, int scale)
   {
      switch (scale)
      {
       case 0: return new _0 (significand);
       case 1: return new _1 (significand);
       case 2: return new _2 (significand);
       case 3: return new _3 (significand);
       case 4: return new _4 (significand);
       case 5: return new _5 (significand);
       case 6: return new _6 (significand);
       case 7: return new _7 (significand);
       case 8: return new _8 (significand);
       case 9: return new _9 (significand);
       case 10: return new _10 (significand);
       default:
         if (scale >= 0)
            return new _N (significand, scale);
         else
            throw new IllegalArgumentException (
               "FixedDec scale must be >= 0: " + String.valueOf (scale));
      }
   }

   /**
      Creates a fixedDec({@code scale}) instance from a Decimal

      @param value a decimal
      @return a fixed decimal value
   */

   public static FixedDec valueOf (Decimal value)
   {
      return valueOf (value, - value.getExponent ()); 
   }
   
   /**
      Creates a fixedDec({@code scale}) instance from a Decimal

      @param value a decimal
      @param scale a scale, the scale must be >= 0
      @return a fixed decimal value
   */

   public static FixedDec valueOf (Decimal value, int scale)
   {
      int fromScale = - value.getExponent ();
      return valueOf (value.getSignificand (), fromScale, scale);
   }

   /**
      Creates a fixed decimal based on another fixed decimal value,
      possibly rescaled.

      @param other a fixed decimal
      @param scale a scale
      @return a fixed decimal value. If the scale of the {@code other}
      parameter is the same as the specified {@code scale}, the same instance
      is returned
   */

   public static FixedDec valueOf (FixedDec other, int scale)
   {
      int otherScale = other.getScale ();
      if (scale == otherScale)
         return other;
      else
         return valueOf (other.getSignificand (), otherScale, scale);
   }

   /**
      Creates a fixed decimal instance by parsing the specified string.

      @param s a string on the same format accepted by {@link Decimal}
      @param scale a scale
      @return a fixed decimal value
   */

   public static FixedDec valueOf (String s, int scale)
   {
      BigDecimal bd = new BigDecimal (s);
      int fromScale = bd.scale ();
      if (fromScale < 0)
      {
         bd = bd.setScale (0);
         fromScale = 0;
      }

      return valueOf (bd.unscaledValue ().longValue (), fromScale, scale);
   }

   /**
      Creates a fixed decimal instance by parsing the specified string.

      @param s a string on the same format accepted by {@link Decimal}
      @return a fixed decimal value
   */
   
   public static FixedDec valueOf (String s)
   {
      BigDecimal bd = new BigDecimal (s);
      int fromScale = bd.scale ();
      if (fromScale < 0)
      {
         bd = bd.setScale (0);
         fromScale = 0;
      }

      return valueOf (bd.unscaledValue ().longValue (), fromScale, fromScale);
   }

   /**
      Rescales a significand between two scales

      @param significand a significand
      @param fromScale a source scale
      @param toScale a target scale
      @return a rescaled significand
   */
   
   public static long rescale (long significand, int fromScale, int toScale)
   {
      int diff = toScale - fromScale;
      if (diff == 0 || significand == 0)
      {
         return significand;
      }
      else if (diff > 0)
      {
         if (diff == 7)
            return significand * 10000000L;
         else if (diff == 2)
            return significand * 100L;
         else if (diff == 8)
            return significand * 100000000L;
         else if (diff == 1)
            return significand * 10L;
         else if (diff == 3)
            return significand * 1000L;
         else if (diff == 4)
            return significand * 10000L;
         else if (diff == 5)
            return significand * 100000L;
         else if (diff == 6)
            return significand * 1000000L;
         else if (diff == 9)
            return significand * 1000000000L;
         else
            return significand * (long)Math.pow (10, diff);
      }
      else
      {
         if (diff == -7)
            return significand / 10000000L;
         else if (diff == -2)
            return significand / 100L;
         else if (diff == -8)
            return significand / 100000000L;
         else if (diff == -1)
            return significand / 10L;
         else if (diff == -3)
            return significand / 1000L;
         else if (diff == -4)
            return significand / 10000L;
         else if (diff == -5)
            return significand / 100000L;
         else if (diff == -6)
            return significand / 1000000L;
         else if (diff == -9)
            return significand / 1000000000L;
         else
            return significand / (long)Math.pow (10, - diff);
      }
   }

   public static final class _0 extends FixedDec
   {
      public final static int Scale = 0;
      public _0 (long sig) { super (sig); }
      @Override public int getScale () { return 0; }
      public static _0 valueOf (long value)
      {
         return new _0 (value);
      }

      public static _0 valueOf (Decimal v)
      {
         return new _0 (rescale (v.getSignificand (), - v.getExponent (), 0));
      }
   
      public static _0 valueOf (FixedDec v)
      {
         if (v instanceof _0)
            return (_0)v;
         else
            return new _0 (rescale (v.getSignificand (), v.getScale (), 0));
      }
   
      public static _0 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (0);
         return new _0 (bd.unscaledValue ().longValue ());
      }

      public static _0 getInstance (long significand)
      {
         return new _0 (significand);
      }
   }

   public static final class _1 extends FixedDec
   {
      public final static int Scale = 1;
      public _1 (long sig) { super (sig); }
      @Override public int getScale () { return 1; }
      public static _1 valueOf (long value)
      {
         return new _1 (rescale (value, 0, 1));
      }

      public static _1 valueOf (Decimal v)
      {
         return new _1 (rescale (v.getSignificand (), - v.getExponent (), 1));
      }
   
      public static _1 valueOf (FixedDec v)
      {
         if (v instanceof _1)
            return (_1)v;
         else
            return new _1 (rescale (v.getSignificand (), v.getScale (), 1));
      }
   
      public static _1 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (1);
         return new _1 (bd.unscaledValue ().longValue ());
      }

      public static _1 getInstance (long significand)
      {
         return new _1 (significand);
      }
   }

   public static final class _2 extends FixedDec
   {
      public final static int Scale = 2;
      public _2 (long sig) { super (sig); }
      @Override public int getScale () { return 2; }
      public static _2 valueOf (long value)
      {
         return new _2 (rescale (value, 0, 2));
      }

      public static _2 valueOf (Decimal v)
      {
         return new _2 (rescale (v.getSignificand (), - v.getExponent (), 2));
      }
   
      public static _2 valueOf (FixedDec v)
      {
         if (v instanceof _2)
            return (_2)v;
         else
            return new _2 (rescale (v.getSignificand (), v.getScale (), 2));
      }
   
      public static _2 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (2);
         return new _2 (bd.unscaledValue ().longValue ());
      }

      public static _2 getInstance (long significand)
      {
         return new _2 (significand);
      }
   }

   public static final class _3 extends FixedDec
   {
      public final static int Scale = 3;
      public _3 (long sig) { super (sig); }
      @Override public int getScale () { return 3; }
      public static _3 valueOf (long value)
      {
         return new _3 (rescale (value, 0, 3));
      }

      public static _3 valueOf (Decimal v)
      {
         return new _3 (rescale (v.getSignificand (), - v.getExponent (), 3));
      }
   
      public static _3 valueOf (FixedDec v)
      {
         if (v instanceof _3)
            return (_3)v;
         else
            return new _3 (rescale (v.getSignificand (), v.getScale (), 3));
      }
   
      public static _3 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (3);
         return new _3 (bd.unscaledValue ().longValue ());
      }

      public static _3 getInstance (long significand)
      {
         return new _3 (significand);
      }
   }

   public static final class _4 extends FixedDec
   {
      public final static int Scale = 4;
      public _4 (long sig) { super (sig); }
      @Override public int getScale () { return 4; }
      public static _4 valueOf (long value)
      {
         return new _4 (rescale (value, 0, 4));
      }

      public static _4 valueOf (Decimal v)
      {
         return new _4 (rescale (v.getSignificand (), - v.getExponent (), 4));
      }
   
      public static _4 valueOf (FixedDec v)
      {
         if (v instanceof _4)
            return (_4)v;
         else
            return new _4 (rescale (v.getSignificand (), v.getScale (), 4));
      }
   
      public static _4 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (4);
         return new _4 (bd.unscaledValue ().longValue ());
      }

      public static _4 getInstance (long significand)
      {
         return new _4 (significand);
      }
   }

   public static final class _5 extends FixedDec
   {
      public final static int Scale = 5;
      public _5 (long sig) { super (sig); }
      @Override public int getScale () { return 5; }
      public static _5 valueOf (long value)
      {
         return new _5 (rescale (value, 0, 5));
      }

      public static _5 valueOf (Decimal v)
      {
         return new _5 (rescale (v.getSignificand (), - v.getExponent (), 5));
      }
   
      public static _5 valueOf (FixedDec v)
      {
         if (v instanceof _5)
            return (_5)v;
         else
            return new _5 (rescale (v.getSignificand (), v.getScale (), 5));
      }
   
      public static _5 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (5);
         return new _5 (bd.unscaledValue ().longValue ());
      }

      public static _5 getInstance (long significand)
      {
         return new _5 (significand);
      }
   }

   public static final class _6 extends FixedDec
   {
      public final static int Scale = 6;
      public _6 (long sig) { super (sig); }
      @Override public int getScale () { return 6; }
      public static _6 valueOf (long value)
      {
         return new _6 (rescale (value, 0, 6));
      }

      public static _6 valueOf (Decimal v)
      {
         return new _6 (rescale (v.getSignificand (), - v.getExponent (), 6));
      }
   
      public static _6 valueOf (FixedDec v)
      {
         if (v instanceof _6)
            return (_6)v;
         else
            return new _6 (rescale (v.getSignificand (), v.getScale (), 6));
      }
   
      public static _6 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (6);
         return new _6 (bd.unscaledValue ().longValue ());
      }

      public static _6 getInstance (long significand)
      {
         return new _6 (significand);
      }
   }

   public static final class _7 extends FixedDec
   {
      public final static int Scale = 7;
      public _7 (long sig) { super (sig); }
      @Override public int getScale () { return 7; }
      public static _7 valueOf (long value)
      {
         return new _7 (rescale (value, 0, 7));
      }

      public static _7 valueOf (Decimal v)
      {
         return new _7 (rescale (v.getSignificand (), - v.getExponent (), 7));
      }
   
      public static _7 valueOf (FixedDec v)
      {
         if (v instanceof _7)
            return (_7)v;
         else
            return new _7 (rescale (v.getSignificand (), v.getScale (), 7));
      }
   
      public static _7 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (7);
         return new _7 (bd.unscaledValue ().longValue ());
      }

      public static _7 getInstance (long significand)
      {
         return new _7 (significand);
      }
   }

   public static final class _8 extends FixedDec
   {
      public final static int Scale = 8;
      public _8 (long sig) { super (sig); }
      @Override public int getScale () { return 8; }
      public static _8 valueOf (long value)
      {
         return new _8 (rescale (value, 0, 8));
      }

      public static _8 valueOf (Decimal v)
      {
         return new _8 (rescale (v.getSignificand (), - v.getExponent (), 8));
      }
   
      public static _8 valueOf (FixedDec v)
      {
         if (v instanceof _8)
            return (_8)v;
         else
            return new _8 (rescale (v.getSignificand (), v.getScale (), 8));
      }
   
      public static _8 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (8);
         return new _8 (bd.unscaledValue ().longValue ());
      }

      public static _8 getInstance (long significand)
      {
         return new _8 (significand);
      }
   }

   public static final class _9 extends FixedDec
   {
      public final static int Scale = 9;
      public _9 (long sig) { super (sig); }
      @Override public int getScale () { return 9; }
      public static _9 valueOf (long value)
      {
         return new _9 (rescale (value, 0, 9));
      }

      public static _9 valueOf (Decimal v)
      {
         return new _9 (rescale (v.getSignificand (), - v.getExponent (), 9));
      }
   
      public static _9 valueOf (FixedDec v)
      {
         if (v instanceof _9)
            return (_9)v;
         else
            return new _9 (rescale (v.getSignificand (), v.getScale (), 9));
      }
   
      public static _9 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (9);
         return new _9 (bd.unscaledValue ().longValue ());
      }

      public static _9 getInstance (long significand)
      {
         return new _9 (significand);
      }
   }

   public static final class _10 extends FixedDec
   {
      public final static int Scale = 10;
      public _10 (long sig) { super (sig); }
      @Override public int getScale () { return 10; }
      public static _10 valueOf (long value)
      {
         return new _10 (rescale (value, 0, 10));
      }

      public static _10 valueOf (Decimal v)
      {
         return new _10 (rescale (v.getSignificand (), - v.getExponent (), 10));
      }
   
      public static _10 valueOf (FixedDec v)
      {
         if (v instanceof _10)
            return (_10)v;
         else
            return new _10 (rescale (v.getSignificand (), v.getScale (), 10));
      }
   
      public static _10 valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         bd = bd.setScale (10);
         return new _10 (bd.unscaledValue ().longValue ());
      }

      public static _10 getInstance (long significand)
      {
         return new _10 (significand);
      }
   }

   public static final class _N extends FixedDec
   {
      public _N (long sig, int scale) { super (sig); this.scale = scale; }
      @Override public int getScale () { return scale; }

      public static _N valueOf (long value)
      {
         return new _N (value, 0);
      }

      public static _N valueOf (Decimal v)
      {
         return new _N (v.getSignificand (), - v.getExponent ());
      }
   
      public static _N valueOf (FixedDec v)
      {
         if (v instanceof _N)
            return (_N)v;
         else
            return new _N (v.getSignificand (), v.getScale ());
      }
   
      public static _N valueOf (String s)
      {
         BigDecimal bd = new BigDecimal (s);
         return new _N (bd.unscaledValue ().longValue (), bd.scale ());
      }

      public static _N getInstance (long significand, int scale)
      {
         return new _N (significand, scale);
      }

      private final int scale;
   }

   private final long significand;
}