package nxm.sys.lib;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.util.Arrays;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.Units;

/** Class to perform alignable Decimal Formatting with Units. This is used internally by the numeric
    valuators and has special methods used for character alignment in a GUI. Most users should use
    the {@link MFormat} class for general formatting.

    @author Jeff Schoen
    @see MFormat
*/
public class Format extends DecimalFormat {

  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 5256251046960840573L;
  /** format pattern string */
  private String pattern;
  /** format units string */
  private String units = "";
  /** position of integer part of the formatted number */
  private transient FieldPosition fp = new FieldPosition(INTEGER_FIELD);
  /** fones */
  private int fones;
  /** ones */
  private int ones;
  /** exponent */
  private int exp;
  /** Determines whether or not a multiplier is used */
  private int imult;
  /** selected digit */
  private int digitSel;
  /** multiplier */
  private double mult=1;
  /** multiplier in character form */
  private char cmult=' ';
  /** Is the digit selected?*/
  private boolean isSelected;
  /** hrs:min:sec format */
  private boolean hms;
  /** deg'min'sec format */
  private boolean dms;
  /** decimal places of STD Time Format */
  private int dpHMS;
  /** Enhanced Simple Date Format added in 4.1.3 for NXM-3664*/
  private boolean sdf;
  /** Simple Date Format display pattern */
  private String  sdfDisplayPattern; // NXM-3664 SimpleDateFormat support
  /** Simple Date Format needs the full 2 digits for increment and decrement to work correctly */
  private char[]  SDF_NEEDS_2 = {'H','K','L','Y','d','h','k','m','s','y'};
  /** Simple Date Format needs 3 digits for increment and decrement to work correctly */
  private char    SDF_NEEDS_3 = 'D';

  /** Format Default Constructor */
  public Format () {
  }

  /** Constructor with given pattern
      @param pattern A Java format pattern or HH:MM:SS or DMS
  */
  public Format (String pattern) {
    this();
    this.setFormat(pattern);
  }

  /** Set the format to a Java pattern or a special pattern.
     <pre>
       Special patterns and characters are:
          HH:MM:SS - Show in hours:minutes:seconds
          DMS      - Show in deg'min'sec format (i.e 33'22'11)
          ?        - Question marks are replaced with blanks and trigger the
                     use of a multiplier

       Units are defined as the String to the right of the last blank space in the
       input String. (e.g. "## MHz" the units are MHz).
     </pre>
    @param pattern A Java format pattern or HH:MM:SS or DMS
  */
  public void setFormat (String pattern) {
    this.pattern = pattern;

    boolean hmsWithFSec = pattern.matches("HH:MM:SS\\.[S]+");
    hms = hmsWithFSec || pattern.equals("HH:MM:SS");
    if (hmsWithFSec) {
      dpHMS = pattern.length() - pattern.indexOf('.') -1;
    }
    dms = pattern.equals("DMS");
    sdf = pattern.startsWith("SDF:");
    if (sdf) pattern = fixSDF(pattern);
    fones = pattern.indexOf('.');
    if (fones<0) fones = pattern.lastIndexOf('#')+1;
    if (fones<=0) fones = pattern.lastIndexOf('0')+1;
    imult = pattern.indexOf('?');
    if (imult>=0) pattern = pattern.replace('?',' ');
    if (imult<0) imult = 0;
    if (!hms && !dms) applyPattern(pattern);
    int space = pattern.lastIndexOf(' ');
    if (space >=0 ) units=pattern.substring(space+1);
  }

  /** Fix the SimpleDateFormat so it works well with increment and decrement */
  private String fixSDF (String pattern) {
    int maxFracSec = 0;
    int currFracSec = 0;
    // no Time Zone support
    pattern = pattern.replaceAll("Z","");
    pattern = pattern.replaceAll("z","");
    pattern = pattern.replaceAll("X","");
    pattern.trim();
    for (int i=4; i < pattern.length(); i++) {
      char c = pattern.charAt(i);
      boolean needsTwo = Arrays.binarySearch(SDF_NEEDS_2, c) != -1;
      int numCharRemaining = pattern.length() - 1 - i;
      if (c == 'S') {
        currFracSec++;
        maxFracSec = Math.max(maxFracSec, currFracSec);
      }
      else if (needsTwo) {
        if (numCharRemaining == 0) {
          pattern = pattern + c;
        }
        else if (pattern.charAt(i+1)!=c) {
          pattern = pattern.substring(0,i+1) + c + pattern.substring(i+1);
        }
        i++; // skip next char
        currFracSec = 0;
      }
      else if (c == SDF_NEEDS_3) {
        if (numCharRemaining == 0) {
          pattern = pattern + c + c;
        }
        else if (pattern.charAt(i+1)!=c) {
          pattern = pattern.substring(0,i+1) + c + c + pattern.substring(i+1);
        }
        else if (numCharRemaining == 1) {
          pattern = pattern + c;
        }
        else if (pattern.charAt(i+2)!=c) {
          pattern = pattern.substring(0,i+2) + c + pattern.substring(i+2);
        }
        i += 2; // skip 2 chars
        currFracSec = 0;
      }
      else {
        currFracSec = 0;
      }
    }

    this.pattern = pattern;
    if (maxFracSec > 0) {
      char[] arrayS = new char[maxFracSec];
      Arrays.fill(arrayS, 'S');

      this.sdfDisplayPattern = "yyyy:MM:dd::HH:mm:ss." + new String(arrayS);
    }
    else {
      this.sdfDisplayPattern = "yyyy:MM:dd::HH:mm:ss";
    }
    return pattern;
  }

  /** Get number  on HMS with fractional seconds
   *
   *  @return number of fractional seconds
   */
  @InternalUseOnly
  public int getDecPlacesHMS () {
    return this.dpHMS;
  }

  /** Get the STD string matching the SDF string
   *  @return standard yyyy:MM:dd::HH:mm:ss[.SSSS]
   */
  public String getSdfDisplayPattern () {
    return this.sdfDisplayPattern;
  }

  /** Get the pattern string.
   *  @return pattern string */
  public String getFormat() { return pattern; }

  /** Sets the exponent in the given a number or multiplier string.
     <pre>
       Arguments can be passed in three ways:
         1) n, where n is one of {-12,-9,-6,-3,3,6,9,12}: (e.g. 6 is Mega)
         2) One of { "pico", "nano", "micro", "milli","Kilo", "Mega", "Giga", "Tera" }
         --OR--
         3) One of {'p','n','u','m','K','M','G','T'}
     </pre>
     @param obj Number or string representing the  multiplier
  */
  public void setExp (Object obj) {
    int imult = 0;
    if (obj instanceof Data) {
      int val = ((Data)obj).toL();
      if (val < -12 || val > 12 || (val%3 > 0)) {
        Shell.warning("Invalid argument for Format.setExp: " + obj);
        return;
      }
      else imult = val;
    }
    else if (obj instanceof String) {
      int index = -1;
      String arg = ((String)obj).trim();
      if (arg.length() > 1) {
        for (int i = 0; i < Units.expNames.length ; i++) {
          if (Units.expNames[i].equalsIgnoreCase(arg)) {
            imult = Units.expValues[i];
            index = i;
            break;
          }
        }
      }
      else {
        char cmult = arg.charAt(0);
        for (int i = 0; i < Units.expAbbr.length ; i++) {
          if (Units.expAbbr[i] == cmult) {
            imult = Units.expValues[i];
            index = i;
            break;
          }
        }
      }
      if (index == -1) {
        Shell.warning("Invalid argument for Format.setExp: " + arg);
        return;
      }
    }
    setExp(imult);
  }

  /** Get the exponent
   *  @return the exponent */
  public int getExp () { return exp; }

  /** Set the best engineering exponent for a range of numbers
    @param dmin Minimum of the range
    @param dmax Maximum of the range
  */
  public void setExp (double dmin, double dmax) {
    double absmax; int kengr;
    absmax = Math.max( Math.abs(dmin),Math.abs(dmax) );
    if (absmax == 0.0) kengr=0;
    else {
      kengr = (int)( 0.1447648 * Math.log(absmax*.5) );
      //if (absmax<1.0) kengr--;
    }
    setExp (kengr*3);
  }

  /** Set the exponent.  If not &gt;-12, &lt;12 and a multiple of 3, the multiplier character is '?'.
    @param exp the exponent
  */
  public void setExp (int exp) {
    if (imult==0) exp = 0;
    this.exp = exp;
    mult = Math.pow(10.0,-exp);
    switch (exp) {
     case  0:  cmult=' '; break;
     case  3:  cmult='k'; break;
     case  6:  cmult='M'; break;
     case  9:  cmult='G'; break;
     case 12:  cmult='T'; break;
     case -3:  cmult='m'; break;
     case -6:  cmult='u'; break;
     case -9:  cmult='n'; break;
     case -12: cmult='p'; break;
     default:  cmult='?';
    }
  }

  /** Get the multiplier in String form.
   *  @return multiplier in string form */
  public String getCmult() {
    if (cmult == ' ') {
      return "";
    } else  return "" + cmult;
  }

  /** Get the units in string form.
   *  @return units in string form */
  public String getUnits() {
    return units;
  }

  /** The un-overridable format() call to format a double number.
      @param dval Number to be formatted
      @return String form of double value
   */
  public String form (double dval) {
    String s;
    if (hms) {
      s = Time.toString(dval,Time.STD,dpHMS); // 3.9.4
      ones = s.length();
    }
    else if (dms) {
      s = Convert.deg2dmsUnicode(dval);
      ones = 2;
    }
    else if (sdf) {
      Time t = new Time(dval);
      s = t.toString(pattern.substring(4));
    }
    else {
      StringBuffer sb = new StringBuffer(pattern.length());
      sb = super.format(dval*mult,sb,fp);
      ones = fp.getEndIndex();
      int i = imult + sb.length() - pattern.length();
      if (imult==0);
      else if (exp==0) sb.deleteCharAt(i);
      else sb.setCharAt(i,cmult);
      s = sb.toString();
      i = s.indexOf('E');
      if (imult==0 && i>=0) {
        exp = Integer.parseInt(s.substring(i+1));
        if (s.endsWith("E0")) s = s.substring(0,i);
      }
    }
    return s;
  }

  /** Increment the selected digit.
    www.fffEee
    digit is the position of the dot
    Fractional digits (negative) are to the right
    Whole digits (positive) are to the left
  */
  public void incDigitPos () { digitSel++; }

  /** Decrement the selected digit.
    www.fffEee
    digit is the position of the dot
    Fractional digits (negative) are to the right
    Whole digits (positive) are to the left
  */
  public void decDigitPos () { digitSel--; }

  /** Set the digit position.
      @param pos the digit pos
  */
  public void setDigitPos (int pos) {
    int digit = ones-pos+exp;
//System.out.println("Set pos ds="+digitSel+" d="+digit+" e="+exp+" o="+ones+" p="+pos);
    if (pos<ones) digit--;
    isSelected = pos>=0 && pos!=ones && !(isSelected && digit==digitSel);
    digitSel = digit;
  }

  /** Get the digit position of currently selected digit.
   *  @return the position of the currently selected digit */
  public int getDigitPos() {
    if (!isSelected) return -1;
    int digit = (digitSel-exp);
    int pos = ones-digit;
    if (digit>=0) pos--;
//System.out.println("Digit pos ds="+digitSel+" d="+digit+" e="+exp+" o="+ones+" p="+pos);
    return pos;
  }

  /** Get Digit Selected
   *   @return digit selected
   */
  @InternalUseOnly
  public int getDigitSel () {
    return this.digitSel;
  }

  /** Get the digit increment.  Returns 0 if not selected.
   *  @return digit increment or 0 if not selected */
  public double getDigitInc() {
    return (isSelected)? getDigitInc(digitSel-exp) : 0;
  }
  /** Test whether a digit is selected.
   *  @return true if digit selected */
  public boolean isDigitSel() { return isSelected; }

  /** Get the digit position given a digit (ones - digit) .
   *  @param digit digit
   *  @return digit position
   */
  public int getDigitPos (int digit) { return ones-digit; }

  /** Get the digit index (ones-pos).
   *  @param pos position
   *  @return digit index
   */
  public int getDigitIndex (int pos) { return ones-pos; }

  /** Gets the increment that corresponds to the given digit. Digit is relative to the ones
      place and 0 corresponds to the ones place and a negative digit indicates right of the ones
      place. For HMS the increment is in seconds and for DMS the increment is in degrees.
      @param digit The digit place 0 to n
      @return the increment that corresponds to the given digit
  */
  public double getDigitInc (int digit) {
    double inc=0;
//jph -- methinks we do not need this because the else clause contains the same equation
//    if (digit<0) inc = Math.pow(10.0,exp+digit);
//    else
    if (hms) {
      if (digit>=0) switch (digit) {
        case 0: inc=1; break;
        case 1: inc=10; break;
        case 3: inc=60; break;
        case 4: inc=600; break;
        case 6: inc=3600; break;
        case 7: inc=36000; break;
        case 10: inc=86400; break;
        case 11: inc=864000; break;
      }
      else {
        inc = Math.pow(10,digit); // get the fractional second increment
      }
    }
    else if (dms) switch (digit) {
        case  0: inc=1;       break;
        case  1: inc=10;      break;
        case -1: inc=1/6.0;   break;
        case -2: inc=1/60.0;  break;
        case -4: inc=1/360.0; break;
        case -5: inc=1/3600.0;break;
    }
    else inc = Math.pow(10.0,exp+digit);
    return inc;
  }

  /** @deprecated Since NeXtMidas 2.5.0: Use the {@link MFormat} class.
   *  @param format Mode and Type
   *  @param units  units (ex: LONGITUDE)
   *  @return Format
   */
  @Deprecated public static Format getDefaultFormatFor (String format, int units) {
    // final char mode = format.charAt(0);
    final char type = format.charAt(1);
    Format fmt = null;

    if (Data.isString((byte)type)) { /* ASCII/String */
      fmt = null;

    } else {
      String unitsStr = DataFile.getUnitsNameShort(units);
      String floatStr = null; /* The format to use for float/double values. */
      String intStr   = null; /* The format to use for long/int/short/byte values. */

      switch(units) {
        case Units.LONGITUDE:  /* FALLTHROUGH */
        case Units.LATITUDE:   floatStr = "##0.0000";      intStr = "###"; unitsStr = ""; break;
        default:               floatStr = "##0.#####E000"; intStr = "0";                  break;
      }

      if (unitsStr.length() > 0) {
        unitsStr = "'" + unitsStr + "'"; /* Protect the units. */
      }

      if ((type == DataTypes.DOUBLE) || (type == DataTypes.FLOAT)) {
        fmt = new nxm.sys.lib.Format(floatStr + unitsStr);
      } else {
        fmt = new nxm.sys.lib.Format(intStr + unitsStr);
      }
    }

    return fmt;
  }

  /** Internal Use Only
   *  @return Field Position
   */
  @InternalUseOnly
  public FieldPosition getFieldPosition () {
    return fp;
  }

  /** Is the Format a SimpleDateFormat?
   *  @return true if it is a SDF
   */
  @InternalUseOnly
  public boolean isSimpleDateFormat () {
    return sdf;
  }

  /** Overwrite default deserialization
  *
  *  @param ois Object Input Strean
  *  @throws ClassNotFoundException Exception thrown by readObject
  *  @throws IOException Exception thrown by readObject
  */
  private void readObject (ObjectInputStream ois) throws ClassNotFoundException, IOException {
    // default deserialization
    ois.defaultReadObject();
    // restore FieldPosition setting
    this.fp = new FieldPosition(INTEGER_FIELD);
  }
}
