/*
 * LengthUnit.java 22 nov. 2008
 *
 * Sweet Home 3D, Copyright (c) 2006-2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.model;

import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * Unit used for lengths.
 */
public enum LengthUnit {
  /**
   * Millimeter unit.
   * @since 2.0
   */
  MILLIMETER {
    private Locale        formatLocale;  
    private String        name;
    private DecimalFormat lengthFormatWithUnit;
    private DecimalFormat lengthFormat;
    private DecimalFormat areaFormatWithUnit;
    
    @Override
    public Format getFormatWithUnit() {
      checkLocaleChange();
      return this.lengthFormatWithUnit;
    }

    @Override
    public Format getAreaFormatWithUnit() {
      checkLocaleChange();
      return this.areaFormatWithUnit;
    }

    @Override
    public Format getFormat() {
      checkLocaleChange();
      return this.lengthFormat;
    }
    
    @Override
    public String getName() {
      checkLocaleChange();
      return this.name;
    }
    
    private void checkLocaleChange() {
      // Instantiate formats if locale changed
      if (!Locale.getDefault().equals(this.formatLocale)) {
        this.formatLocale = Locale.getDefault();  
        ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
        this.name = resource.getString("millimeterUnit");
        this.lengthFormatWithUnit = new MeterFamilyFormat("#,##0 " + this.name, 10);          
        this.lengthFormat = new MeterFamilyFormat("#,##0", 10);
        String squareMeterUnit = resource.getString("squareMeterUnit");
        this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
      }
    }

    @Override
    public float getMagnetizedLength(float length, float maxDelta) {
      return getMagnetizedMeterLength(length, maxDelta);
    }

    @Override
    public float getMinimumLength() {
      return 0.1f;
    }
    
    @Override
    public float getMaximumLength() {
      return 100000f;
    }

    @Override
    public float centimeterToUnit(float length) {
      return length * 10;
    }

    @Override
    public float unitToCentimeter(float length) {
      return length / 10;
    }
  }, 
  
  /**
   * Centimeter unit.
   */
  CENTIMETER {
    private Locale        formatLocale;  
    private String        name;
    private DecimalFormat lengthFormatWithUnit;
    private DecimalFormat lengthFormat;
    private DecimalFormat areaFormatWithUnit;
    
    @Override
    public Format getFormatWithUnit() {
      checkLocaleChange();
      return this.lengthFormatWithUnit;
    }

    @Override
    public Format getAreaFormatWithUnit() {
      checkLocaleChange();
      return this.areaFormatWithUnit;
    }

    @Override
    public Format getFormat() {
      checkLocaleChange();
      return this.lengthFormat;
    }
    
    @Override
    public String getName() {
      checkLocaleChange();
      return this.name;
    }
    
    private void checkLocaleChange() {
      // Instantiate formats if locale changed
      if (!Locale.getDefault().equals(this.formatLocale)) {
        this.formatLocale = Locale.getDefault();  
        ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
        this.name = resource.getString("centimeterUnit");
        this.lengthFormatWithUnit = new MeterFamilyFormat("#,##0.# " + this.name, 1);          
        this.lengthFormat = new MeterFamilyFormat("#,##0.#", 1);
        String squareMeterUnit = resource.getString("squareMeterUnit");
        this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
      }
    }

    @Override
    public float getMagnetizedLength(float length, float maxDelta) {
      return getMagnetizedMeterLength(length, maxDelta);
    }

    @Override
    public float getMinimumLength() {
      return 0.1f;
    }
    
    @Override
    public float getMaximumLength() {
      return 100000f;
    }

    @Override
    public float centimeterToUnit(float length) {
      return length;
    }

    @Override
    public float unitToCentimeter(float length) {
      return length;
    }
  }, 
  
  /**
   * Meter unit.
   * @since 2.0
   */
  METER {
    private Locale        formatLocale;  
    private String        name;
    private DecimalFormat lengthFormatWithUnit;
    private DecimalFormat lengthFormat;
    private DecimalFormat areaFormatWithUnit;
    
    @Override
    public Format getFormatWithUnit() {
      checkLocaleChange();
      return this.lengthFormatWithUnit;
    }

    @Override
    public Format getAreaFormatWithUnit() {
      checkLocaleChange();
      return this.areaFormatWithUnit;
    }

    @Override
    public Format getFormat() {
      checkLocaleChange();
      return this.lengthFormat;
    }
    
    @Override
    public String getName() {
      checkLocaleChange();
      return this.name;
    }
    
    private void checkLocaleChange() {
      // Instantiate formats if locale changed
      if (!Locale.getDefault().equals(this.formatLocale)) {
        this.formatLocale = Locale.getDefault();  
        ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
        this.name = resource.getString("meterUnit");
        this.lengthFormatWithUnit = new MeterFamilyFormat("#,##0.00# " + this.name, 0.01f);          
        this.lengthFormat = new MeterFamilyFormat("#,##0.00#", 0.01f);
        String squareMeterUnit = resource.getString("squareMeterUnit");
        this.areaFormatWithUnit = new SquareMeterAreaFormatWithUnit(squareMeterUnit);
      }
    }

    @Override
    public float getMagnetizedLength(float length, float maxDelta) {
      return getMagnetizedMeterLength(length, maxDelta);
    }

    @Override
    public float getMinimumLength() {
      return 0.1f;
    }

    @Override
    public float getMaximumLength() {
      return 100000f;
    }

    @Override
    public float centimeterToUnit(float length) {
      return length / 100;
    }

    @Override
    public float unitToCentimeter(float length) {
      return length * 100;
    }
  }, 
  
  /**
   * Foot/Inch unit followed by fractions.
   */
  INCH {
    private Locale        formatLocale;
    private String        name;
    private DecimalFormat lengthFormat;
    private DecimalFormat areaFormatWithUnit;

    @Override
    public Format getFormatWithUnit() {
      checkLocaleChange();
      return this.lengthFormat;
    }

    @Override
    public Format getFormat() {
      return getFormatWithUnit();
    }

    @Override
    public Format getAreaFormatWithUnit() {
      checkLocaleChange();
      return this.areaFormatWithUnit;
    }
    
    @Override
    public String getName() {
      checkLocaleChange();
      return this.name;
    }
    
    private void checkLocaleChange() {
      // Instantiate format if locale changed
      if (!Locale.getDefault().equals(this.formatLocale)) {
        this.formatLocale = Locale.getDefault();  
        ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
        this.name = resource.getString("inchUnit");
        
        // Create format for feet and inches
        final MessageFormat footFormat = new MessageFormat(resource.getString("footFormat"));
        final MessageFormat footInchFormat = new MessageFormat(resource.getString("footInchFormat"));
        final MessageFormat footInchEighthFormat = new MessageFormat(resource.getString("footInchEighthFormat"));
        final String        footInchSeparator = resource.getString("footInchSeparator");
        final NumberFormat  footNumberFormat = NumberFormat.getIntegerInstance();
        final NumberFormat  inchNumberFormat = NumberFormat.getNumberInstance();
        final char [] inchFractionCharacters = {'\u215b',   // 1/8
                                                '\u00bc',   // 1/4  
                                                '\u215c',   // 3/8
                                                '\u00bd',   // 1/2
                                                '\u215d',   // 5/8
                                                '\u00be',   // 3/4
                                                '\u215e'};  // 7/8        
        this.lengthFormat = new DecimalFormat("0.000\"") {
            @Override
            public StringBuffer format(double number, StringBuffer result,
                                       FieldPosition fieldPosition) {
              double feet = Math.floor(centimeterToFoot((float)number));              
              float remainingInches = centimeterToInch((float)number - footToCentimeter((float)feet));
              if (remainingInches >= 11.9375f) {
                feet++;
                remainingInches -= 12;
              }
              fieldPosition.setEndIndex(fieldPosition.getEndIndex() + 1);
              // Format remaining inches only if it's larger that 0.0005
              if (remainingInches >= 0.0005f) {
                // Try to format decimals with 1/8, 1/4, 1/2 fractions first
                int integerPart = (int)Math.floor(remainingInches);
                float fractionPart = remainingInches - integerPart;
                int eighth = Math.round(fractionPart * 8); 
                if (eighth == 0 || eighth == 8) {
                  footInchFormat.format(new Object [] {feet, Math.round(remainingInches * 8) / 8f}, result, fieldPosition);
                } else { 
                  footInchEighthFormat.format(new Object [] {feet, integerPart, inchFractionCharacters [eighth - 1]}, result, fieldPosition);
                }
              } else {
                footFormat.format(new Object [] {feet}, result, fieldPosition);
              }
              return result;
            }
            
            @Override
            public Number parse(String text, ParsePosition parsePosition) {
              double value = 0;
              ParsePosition numberPosition = new ParsePosition(parsePosition.getIndex());
              skipWhiteSpaces(text, numberPosition);
              // Parse feet
              int quoteIndex = text.indexOf('\'', parsePosition.getIndex());
              if (quoteIndex != -1) {
                Number feet = footNumberFormat.parse(text, numberPosition);
                if (feet == null) {
                  parsePosition.setErrorIndex(numberPosition.getErrorIndex());
                  return null;
                }
                skipWhiteSpaces(text, numberPosition);
                if (numberPosition.getIndex() != quoteIndex) {
                  parsePosition.setErrorIndex(numberPosition.getIndex());
                  return null;
                }
                value = footToCentimeter(feet.intValue());                
                numberPosition = new ParsePosition(quoteIndex + 1);
                skipWhiteSpaces(text, numberPosition);
                // Test optional foot inch separator
                if (numberPosition.getIndex() < text.length()
                    && footInchSeparator.indexOf(text.charAt(numberPosition.getIndex())) >= 0) {
                  numberPosition.setIndex(numberPosition.getIndex() + 1);
                  skipWhiteSpaces(text, numberPosition);
                }
                if (numberPosition.getIndex() == text.length()) {
                  parsePosition.setIndex(text.length());
                  return value;
                }
              } 
              // Parse inches
              Number inches = inchNumberFormat.parse(text, numberPosition);
              if (inches == null) {
                parsePosition.setErrorIndex(numberPosition.getErrorIndex());
                return null;
              }
              value += inchToCentimeter(inches.floatValue());
              // Parse fraction
              skipWhiteSpaces(text, numberPosition);
              if (numberPosition.getIndex() == text.length()) {
                parsePosition.setIndex(text.length());
                return value;
              }
              if (text.charAt(numberPosition.getIndex()) == '\"') {
                parsePosition.setIndex(numberPosition.getIndex() + 1);
                return value;
              }

              char fractionChar = text.charAt(numberPosition.getIndex());              
              for (int i = 0; i < inchFractionCharacters.length; i++) {
                if (inchFractionCharacters [i] == fractionChar) {
                  // Check no decimal fraction was specified
                  int lastDecimalSeparatorIndex = text.lastIndexOf(getDecimalFormatSymbols().getDecimalSeparator(), 
                      numberPosition.getIndex() - 1);
                  if (lastDecimalSeparatorIndex > quoteIndex) {
                    return null;
                  } else {
                    value += inchToCentimeter((i + 1) / 8f);
                    parsePosition.setIndex(numberPosition.getIndex() + 1);
                    skipWhiteSpaces(text, parsePosition);
                    if (parsePosition.getIndex() < text.length() 
                        && text.charAt(parsePosition.getIndex()) == '\"') {
                      parsePosition.setIndex(parsePosition.getIndex() + 1);
                    }
                    return value;
                  }
                }
              }
              
              parsePosition.setIndex(numberPosition.getIndex());
              return value;
            }
            
            /**
             * Increases the index of <code>fieldPosition</code> to skip white spaces. 
             */
            private void skipWhiteSpaces(String text, ParsePosition fieldPosition) {
              while (fieldPosition.getIndex() < text.length()
                  && Character.isWhitespace(text.charAt(fieldPosition.getIndex()))) {
                fieldPosition.setIndex(fieldPosition.getIndex() + 1);
              }
            }
          };
        
        String squareFootUnit = resource.getString("squareFootUnit");
        this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("#,##0 " + squareFootUnit);
      }
    }
    
    @Override
    public float getMagnetizedLength(float length, float maxDelta) {
      return getMagnetizedInchLength(length, maxDelta);
    }

    @Override
    public float getMinimumLength() {        
      return LengthUnit.inchToCentimeter(0.125f);
    }

    @Override
    public float getMaximumLength() {
      return LengthUnit.inchToCentimeter(99974.4f); // 3280 ft
    }

    @Override
    public float centimeterToUnit(float length) {
      return centimeterToInch(length);
    }

    @Override
    public float unitToCentimeter(float length) {
      return inchToCentimeter(length);
    }
  },

  /**
   * Inch unit with decimals.
   * @since 4.0
   */
  INCH_DECIMALS {
    private Locale        formatLocale;
    private String        name;
    private DecimalFormat lengthFormat;
    private DecimalFormat areaFormatWithUnit;

    @Override
    public Format getFormatWithUnit() {
      checkLocaleChange();
      return this.lengthFormat;
    }

    @Override
    public Format getFormat() {
      return getFormatWithUnit();
    }

    @Override
    public Format getAreaFormatWithUnit() {
      checkLocaleChange();
      return this.areaFormatWithUnit;
    }
    
    @Override
    public String getName() {
      checkLocaleChange();
      return this.name;
    }
    
    private void checkLocaleChange() {
      // Instantiate format if locale changed
      if (!Locale.getDefault().equals(this.formatLocale)) {
        this.formatLocale = Locale.getDefault();  
        ResourceBundle resource = ResourceBundle.getBundle(LengthUnit.class.getName());
        this.name = resource.getString("inchUnit");
        
        // Create format for inches
        final MessageFormat inchDecimalsFormat = new MessageFormat(resource.getString("inchDecimalsFormat"));
        final NumberFormat  inchNumberFormat = NumberFormat.getNumberInstance();
        this.lengthFormat = new DecimalFormat("0.###") {
            @Override
            public StringBuffer format(double number, StringBuffer result,
                                       FieldPosition fieldPosition) {
              float inches = centimeterToInch((float)number);
              fieldPosition.setEndIndex(fieldPosition.getEndIndex() + 1);
              inchDecimalsFormat.format(new Object [] {inches}, result, fieldPosition);
              return result;
            }
            
            @Override
            public Number parse(String text, ParsePosition parsePosition) {
              ParsePosition numberPosition = new ParsePosition(parsePosition.getIndex());
              skipWhiteSpaces(text, numberPosition);
              // Parse inches
              Number inches = inchNumberFormat.parse(text, numberPosition);
              if (inches == null) {
                parsePosition.setErrorIndex(numberPosition.getErrorIndex());
                return null;
              }
              double value = inchToCentimeter(inches.floatValue());
              // Parse "
              skipWhiteSpaces(text, numberPosition);
              if (numberPosition.getIndex() < text.length() 
                  && text.charAt(numberPosition.getIndex()) == '\"') {
                parsePosition.setIndex(numberPosition.getIndex() + 1);
              } else {
                parsePosition.setIndex(numberPosition.getIndex());
              }
              return value;
            }
            
            /**
             * Increases the index of <code>fieldPosition</code> to skip white spaces. 
             */
            private void skipWhiteSpaces(String text, ParsePosition fieldPosition) {
              while (fieldPosition.getIndex() < text.length()
                  && Character.isWhitespace(text.charAt(fieldPosition.getIndex()))) {
                fieldPosition.setIndex(fieldPosition.getIndex() + 1);
              }
            }
          };
        
        String squareFootUnit = resource.getString("squareFootUnit");
        this.areaFormatWithUnit = new SquareFootAreaFormatWithUnit("#,##0.## " + squareFootUnit);
      }
    }
    
    @Override
    public float getMagnetizedLength(float length, float maxDelta) {
      return getMagnetizedInchLength(length, maxDelta);
    }

    @Override
    public float getMinimumLength() {        
      return LengthUnit.inchToCentimeter(0.125f);
    }

    @Override
    public float getMaximumLength() {
      return LengthUnit.inchToCentimeter(99974.4f); // 3280 ft
    }

    @Override
    public float centimeterToUnit(float length) {
      return centimeterToInch(length);
    }

    @Override
    public float unitToCentimeter(float length) {
      return inchToCentimeter(length);
    }
  };

  /**
   * Returns the <code>length</code> given in centimeters converted to inches.
   */
  public static float centimeterToInch(float length) {
    return length / 2.54f;
  }

  /**
   * Returns the <code>length</code> given in centimeters converted to feet.
   */
  public static float centimeterToFoot(float length) {
    return length / 2.54f / 12;
  }
  
  /**
   * Returns the <code>length</code> given in inches converted to centimeters.
   */
  public static float inchToCentimeter(float length) {
    return length * 2.54f;
  }
  
  /**
   * Returns the <code>length</code> given in feet converted to centimeters.
   */
  public static float footToCentimeter(float length) {
    return length * 2.54f * 12;
  }
  
  /**
   * Returns a format able to format lengths with their localized unit.
   */
  public abstract Format getFormatWithUnit(); 

  /**
   * Returns a format able to format lengths.
   */
  public abstract Format getFormat(); 

  /**
   * A decimal format for meter family units.
   */
  private static class MeterFamilyFormat extends DecimalFormat {
    private final float unitMultiplier;

    public MeterFamilyFormat(String pattern, float unitMultiplier) {
      super(pattern);
      this.unitMultiplier = unitMultiplier;
      
    }

    @Override
    public StringBuffer format(double number, StringBuffer result,
                               FieldPosition fieldPosition) {
      // Convert centimeter to millimeter
      return super.format(number * this.unitMultiplier, result, fieldPosition);                
    }

    @Override
    public StringBuffer format(long number, StringBuffer result,
                               FieldPosition fieldPosition) {
      return format((double)number, result, fieldPosition);
    }
    
    @Override
    public Number parse(String text, ParsePosition pos) {
      Number number = super.parse(text, pos);
      if (number == null) {
        return null;
      } else {
        return number.floatValue() / this.unitMultiplier;
      }
    }
  }
  
  /**
   * Returns a format able to format areas with their localized unit.
   */
  public abstract Format getAreaFormatWithUnit();

  /**
   * A decimal format for square meter.
   */
  private static class SquareMeterAreaFormatWithUnit extends DecimalFormat {
    public SquareMeterAreaFormatWithUnit(String squareMeterUnit) {
      super("#,##0.## " + squareMeterUnit);
    }
    
    @Override
    public StringBuffer format(double number, StringBuffer result,
                               FieldPosition fieldPosition) {
      // Convert square centimeter to square meter
      return super.format(number / 10000, result, fieldPosition);                
    }
  }
  
  /**
   * A decimal format for square foot.
   */
  private static class SquareFootAreaFormatWithUnit extends DecimalFormat {
    public SquareFootAreaFormatWithUnit(String pattern) {
      super(pattern);
    }
    
    @Override
    public StringBuffer format(double number, StringBuffer result,
                               FieldPosition fieldPosition) {
      // Convert square centimeter to square foot
      return super.format(number / 929.0304, result, fieldPosition);                
    }
  }
  
  /**
   * Returns a localized name of this unit.
   */
  public abstract String getName();
  
  /**
   * Returns the value close to the given <code>length</code> in centimeter under magnetism. 
   */
  public abstract float getMagnetizedLength(float length, float maxDelta);

  /**
   * Returns the value close to the given length under magnetism for meter units.
   */
  private static float getMagnetizedMeterLength(float length, float maxDelta) {
    // Use a maximum precision of 1 mm depending on maxDelta
    maxDelta *= 2;
    float precision = 1 / 10f;
    if (maxDelta > 100) {
      precision = 100;
    } else if (maxDelta > 10) {
      precision = 10;
    } else if (maxDelta > 5) {
      precision = 5;
    } else if (maxDelta > 1) {
      precision = 1;
    } else if  (maxDelta > 0.5f) {
      precision = 0.5f;
    } 
    float magnetizedLength = Math.round(length / precision) * precision;
    if (magnetizedLength == 0 && length > 0) {
      return length;
    } else {
      return magnetizedLength;
    }
  }
  
  /**
   * Returns the value close to the given length under magnetism for inch units.
   */
  private static float getMagnetizedInchLength(float length, float maxDelta) {
    // Use a maximum precision of 1/8 inch depending on maxDelta
    maxDelta = centimeterToInch(maxDelta) * 2;
    float precision = 1 / 8f;
    if (maxDelta > 6) {
      precision = 6;
    } else if (maxDelta > 3) {
      precision = 3;
    } else if (maxDelta > 1) {
      precision = 1;
    } else if  (maxDelta > 0.5f) {
      precision = 0.5f;
    } else if  (maxDelta > 0.25f) {
      precision = 0.25f;
    }
    float magnetizedLength = inchToCentimeter(Math.round(centimeterToInch(length) / precision) * precision);
    if (magnetizedLength == 0 && length > 0) {
      return length;
    } else {
      return magnetizedLength;
    }
  }


  /**
   * Returns the minimum value for length in centimeter.
   */
  public abstract float getMinimumLength();
  
  /**
   * Returns the maximum value for length in centimeter.
   * @since 3.4
   */
  public abstract float getMaximumLength();
  
  /**
   * Returns the maximum value for elevation in centimeter.
   * @since 3.4
   */
  public float getMaximumElevation() {
    return getMaximumLength() / 10;
  }
  
  /**
   * Returns the <code>length</code> given in centimeters converted 
   * to a value expressed in this unit.
   * @since 2.0
   */
  public abstract float centimeterToUnit(float length);

  /**
   * Returns the <code>length</code> given in this unit converted 
   * to a value expressed in centimeter.
   * @since 2.0
   */
  public abstract float unitToCentimeter(float length);
}