/* $RCSfile$
 * $Author$
 * $Date$
 * $Revision$
 *
 *  FormatStringBuffer: printf style output formatter for Java
 *  Copyright (C) 2002  Antti S. Brax  
 *  All rights reserved.
 *  
 *  Downloaded from: http://www.cs.helsinki.fi/u/abrax/HACK/JAVA/PRINTF.html
 *
 *  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.
 *
 *  - The names of the contributors may not 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.
 */
package org.openscience.cdk.tools;

import org.openscience.cdk.annotations.TestClass;
import org.openscience.cdk.annotations.TestMethod;

import java.text.DecimalFormat;
import java.text.NumberFormat;

/**
 * A class for formatting output similar to the C <tt>printf</tt> command.
 *
 * <P>Some features provided by ANSI C-standard conformant <tt>printfs</tt>
 * are not supported because of language constraints.
 *
 * <P>Supported conversion specifiers are: 'c', 'd', 'e', 'E', 'f', 'g'
 * (works like 'f'), 'i', 'o', 's', 'x' and 'X'.
 *
 * <P>Supported conversion flags are: '#', '0', '-', ' ' (a space) and '+'.
 *
 * <P>Support for conversion flag '*' is under development.
 *
 * @author      Antti S. Brax (asb@iki.fi, base implementation)
 * @author      Fred Long (flong(AT)skcc.org, implemented 'e', 'E' and 'g')
 * @version     1.7
 * @cdk.module  standard
 * @cdk.githash
 * @cdk.license BSD
 */
@TestClass("org.openscience.cdk.tools.FormatStringBufferTest")
public class FormatStringBuffer {

    // ==================================================================== //

    /** Pad with zero instead of space. */
    private static final int ZEROPAD = 1;

    /** Unsigned/signed long. */
    //private static final int SIGN = 2;

    /** Show plus sign. */
    private static final int PLUS = 4;

    /** Space if plus. */
    private static final int SPACE = 8;

    /** Left justified. */
    private static final int LEFT = 16;

    /** Prepend hex digits with '0x' and octal with '0' */
    private static final int SPECIAL = 32;

    /** Use upper case hex digits. */
    private static final int LARGE = 64;

    /** Use scientific notation */
    private static final int SCI = 128;

    /** Use uppercase E */
    private static final int UPPER = 256;

    /** Use grouping character */
    private static final int GROUPING = 512;

    // ==================================================================== //

    /** Format a char. */
    private static final int CHAR = 0;

    /** Format a String. */
    private static final int STRING = 1;

    /** Format a decimal number. */
    private static final int DECIMAL = 2;

    /** Format a floating point number. */
    private static final int FLOAT = 3;

    // ==================================================================== //

    /** The format string. */
    private String format = null;

    /** The buffer. */
    private StringBuffer buffer = null;

    /** The current index. */
    private int index = 0;

    // ==================================================================== //

    /**
     * Create a new <tt>FormatStringBuffer</tt>.
     *
     * @param format the format string.
     */
    public FormatStringBuffer(String format) {
    	reset(format);
    }

    /**
     * Reset this <tt>FormatStringBuffer</tt>.
     *
     * @param format the format string.
     */
    @TestMethod("testReset_String")
    public FormatStringBuffer reset(String format) {
    	reset();
    	this.format = format;
    	return this;
    }

    /**
     * Reset this <tt>FormatStringBuffer</tt> with the format string
     * given in the constructor or last call to <tt>reset(String)</tt>.
     * This is automatically called after <tt>toString()</tt>.
     */
    @TestMethod("testReset")
    public FormatStringBuffer reset() {
    	this.buffer = new StringBuffer();
    	this.index = 0;
    	return this;
    }

    // ==================================================================== //

    /**
     * Get the nect format token from the format string. Copy every
     * character from <tt>format</tt> to <tt>buffer</tt> between
     * <tt>index</tt> and the next format token.
     */
    private Format getFormat() {
    	
    	char ch;
    	
    	while (index < format.length()) {
    		if ((ch = format.charAt(index)) != '%') {
    			buffer.append(ch);
    			index++;
    			continue;
    		}
    		
    		Format fmt = new Format();
    		
    		// Process flags.
    		boolean repeat = true;
    		while (repeat) {
    			
    			if (index + 1 >= format.length())
    				throw new IllegalArgumentException("Malformed format");
    			
    			switch (ch = format.charAt(++index)) { // Skip the first '%'
    			    case '-': fmt.flags |= LEFT; break;
    			    case '+': fmt.flags |= PLUS; break;
    			    case ' ': fmt.flags |= SPACE; break;
    			    case '#': fmt.flags |= SPECIAL; break;
    			    case '0': fmt.flags |= ZEROPAD; break;
    			    case '\'': fmt.flags |= GROUPING; break;
    			    default: repeat = false; break;
    			}
    		}
    		
    		// Get field width.
    		if (Character.isDigit(ch)) {
    			// Explicit number.
    			fmt.fieldWidth = skipDigits();
    		}
    		
    		if (index >= format.length())
    			throw new IllegalArgumentException("Malformed format");
    		
    		// Get precision.
    		if ((ch = format.charAt(index)) == '.') {
    			
    			if (++index >= format.length())
    				throw new IllegalArgumentException("Malformed format");
    			
    			fmt.precision = skipDigits();
    			if (fmt.precision < 0) {
    				fmt.precision = 0;
    			}
    		}
    		
    		if (index >= format.length())
    			throw new IllegalArgumentException("Malformed format");
    		
    		switch (ch = format.charAt(index++)) {
    		    case 'c':
    		    	fmt.type = CHAR;
    		    	return fmt;
    		    case 's':
    		    	fmt.type = STRING;
    		    	return fmt;
    		    case '%':
    		    	buffer.append('%');
    		    	continue;
    		    	
    		    	// Octal, hexadecimal and decimal.
    		    	
    		    case 'o':
    		    	fmt.type = DECIMAL;
    		    	fmt.base = 8;
    		    	return fmt;
    		    case 'X':
    		    	fmt.flags |= LARGE;
    		    case 'x':
    		    	fmt.type = DECIMAL;
    		    	fmt.base = 16;
    		    	return fmt;
    		    case 'd':
    		    case 'i':
    		    	fmt.type = DECIMAL;
    		    	return fmt;
    		    	
    		    	// Floating point
    		    	
    		    case 'f':
    		    case 'g':
    		    	fmt.type = FLOAT;
    		    	return fmt;
    		    case 'e':
    		    	fmt.type = FLOAT;
    		    	fmt.flags |= SCI;
    		    	return fmt;
    		    case 'E':
    		    	fmt.type = FLOAT;
    		    	fmt.flags |= SCI;
    		    	fmt.flags |= UPPER;
    		    	return fmt;
    		    default:
    		    	buffer.append('%');
    		    buffer.append(ch);
    		    continue;
    		}
    	}
    	
    	return null;
    }
    
    /**
     * Skip digits and return the number they form.
     */
    private int skipDigits() {
    	char ch;
    	int i = 0;
    	
    	while (index < format.length()) {
    		if (Character.isDigit(ch = format.charAt(index))) {
    			index++;
    			i = i * 10 + Character.digit(ch, 10);
    		} else {
    			break;
    		}
    	}
    	return i;
    }
    
    // ==================================================================== //
    
    /**
     * Format a <tt>char</tt>.
     */
    @TestMethod("testFormat_char")
    public FormatStringBuffer format(char ch) {
    	
    	Format fmt = getFormat();
    	
    	if (fmt.type != CHAR)
    		throw new IllegalArgumentException("Expected a char format");
    	
    	if ((fmt.flags & LEFT) != LEFT)
    		while (--fmt.fieldWidth > 0)
    			buffer.append(' ');
    	buffer.append(ch);
    	while (--fmt.fieldWidth > 0)
    		buffer.append(' ');
    	
    	return this;
    }
    
    /**
     * Format a <tt>float</tt>.
     */
    @TestMethod("testFormat_floatr")
    public FormatStringBuffer format(float flt) {
    	
    	return format((double)flt);
    	
    }
    
    /**
     * Format a <tt>double</tt>.
     */
    @TestMethod("testFormat_double")
    public FormatStringBuffer format(double dbl) {
    	
    	Format fmt = getFormat();
    	
    	if (fmt.type != FLOAT)
    		throw new IllegalArgumentException("Expected a float format");
    	
    	NumberFormat nf;
    	if ((fmt.flags & SCI) > 0) {
    		nf = new DecimalFormat("0.#E00");
    	} else {
    		nf = NumberFormat.getInstance();
    	}
    	nf.setGroupingUsed((fmt.flags & GROUPING) != 0);
    	if (fmt.precision != -1) {
    		nf.setMaximumFractionDigits(fmt.precision);
    		nf.setMinimumFractionDigits(fmt.precision);
    	} else {
    		nf.setMaximumFractionDigits(Integer.MAX_VALUE);
    		nf.setMinimumFractionDigits(1);
    	}
    	String str = nf.format(dbl);
    	if ((fmt.flags & SCI) == SCI && (fmt.flags & UPPER) == 0) {
    		str = str.replace('E', 'e');
    	}
    	if ((fmt.flags & PLUS) == PLUS && dbl >= 0.0)
    		str = "+" + str;
    	
    	int len = str.length();
    	if ((fmt.flags & LEFT) != LEFT)
    		while (len < fmt.fieldWidth--)
    			buffer.append(' ');
    	
    	for (int i = 0; i < len; ++i)
    		buffer.append(str.charAt(i));
    	
    	while (len < fmt.fieldWidth--)
    		buffer.append(' ');
    	
    	return this;
    }
    
    /**
     * Format a <tt>float</tt>.
     */
    @TestMethod("testFormat_int")
    public FormatStringBuffer format(int i) {
    	
    	return format((long)i);
    	
    }
    
    /**
     * Format a <tt>float</tt>.
     */
    @TestMethod("testFormat_long")
    public FormatStringBuffer format(long l) {
    	
    	Format fmt = getFormat();
    	
    	if (fmt.type != DECIMAL)
    		throw new IllegalArgumentException("Expected a float format");
    	
    	// Decide padding character.
    	char pad = ' ';
    	if ((fmt.flags & ZEROPAD) == ZEROPAD) {
    		pad = '0';
    	}
    	
    	// Convert numberto String.
    	String str;
    	String prefix = "";
    	switch (fmt.base) {
    	    case 8:
    	    	str = Long.toOctalString(l);
    	    	if ((fmt.flags & SPECIAL) == SPECIAL) {
    	    		fmt.fieldWidth -= 1;
    	    		prefix = "0";
    	    	}
    	    	break;
    	    case 16:
    	    	str = Long.toHexString(l);
    	    	if ((fmt.flags & SPECIAL) == SPECIAL) {
    	    		fmt.fieldWidth -= 2;
    	    		prefix = "0x";
    	    	}
    	    	break;
    	    default:
    	    	str = String.valueOf(Math.abs(l));
    	    break;
    	}
    	
    	if ((fmt.flags & LARGE) == LARGE) {
    		str = str.toUpperCase();
    		prefix = prefix.toUpperCase();
    	}
    	
    	int len = str.length();
    	
    	if (l < 0 || (fmt.flags & PLUS) == PLUS) {
    		fmt.fieldWidth--;
    	}
    	
    	// Place the sign character first if zero padding.
    	if ((fmt.flags & ZEROPAD) == ZEROPAD) {
    		if (l < 0 && fmt.base == 10) {
    			buffer.append('-');
    		} else if ((fmt.flags & PLUS) == PLUS && fmt.base == 10) {
    			buffer.append('+');
    		}
    		buffer.append(prefix);
    	}
    	
    	// Pad.
    	if ((fmt.flags & LEFT) != LEFT)
    		while (len < fmt.fieldWidth--)
    			buffer.append(pad);
    	
    	// Place the sign character now if not zero padding.
    	if ((fmt.flags & ZEROPAD) != ZEROPAD) {
    		if (l < 0 && fmt.base == 10) {
    			buffer.append('-');
    		} else if ((fmt.flags & PLUS) == PLUS && fmt.base == 10) {
    			buffer.append('+');
    		}
    		buffer.append(prefix);
    	}
    	
    	for (int i = 0; i < len; ++i)
    		buffer.append(str.charAt(i));
    	
    	while (len < fmt.fieldWidth--)
    		buffer.append(' ');
    	
    	return this;
    }
    
    /**
     * Format a <tt>String</tt>.
     */
    @TestMethod("testFormat_String")
    public FormatStringBuffer format(String str) {
    	
    	if (str == null)
    		str = "<NULL>";
    	
    	Format fmt = getFormat();
    	
    	if (fmt.type != STRING)
    		throw new IllegalArgumentException("Expected a String format");
    	
    	int len = str.length();
    	if (fmt.precision != -1 && len > fmt.precision)
    		len = fmt.precision;
    	
    	if ((fmt.flags & LEFT) != LEFT)
    		while (len < fmt.fieldWidth--)
    			buffer.append(' ');
    	
    	for (int i = 0; i < len; ++i)
    		buffer.append(str.charAt(i));
    	
    	while (len < fmt.fieldWidth--)
    		buffer.append(' ');
    	
    	return this;
    }
    
    // ==================================================================== //
    
    /**
     * Get the result of the formatting. <tt>reset()</tt> is automatically
     * called from this method.
     */
    @TestMethod("testToString")
    public String toString() {
    	
    	if (index < format.length())
    		buffer.append(format.substring(index));
    	
    	String str = buffer.toString();
    	this.reset();
    	
    	return str;
    }
    
    // ==================================================================== //
    
    /**
     * A container class for several format parameters.
     */
    private class Format {
    	public int flags = 0;
    	public int fieldWidth = -1;
    	public int precision = -1;
    	public int type = -1;
    	public int base = 10;
    }
}

