/*
 * %CopyrightBegin%
 * 
 * Copyright Ericsson AB 2000-2009. All Rights Reserved.
 * 
 * The contents of this file are subject to the Erlang Public License,
 * Version 1.1, (the "License"); you may not use this file except in
 * compliance with the License. You should have received a copy of the
 * Erlang Public License along with this software. If not, it can be
 * retrieved online at http://www.erlang.org/.
 * 
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * %CopyrightEnd%
 */
package com.ericsson.otp.erlang;

import java.io.Serializable;

/**
 * Provides a Java representation of Erlang atoms. Atoms can be created from
 * strings whose length is not more than {@link #maxAtomLength maxAtomLength}
 * characters.
 */
public class OtpErlangAtom extends OtpErlangObject implements Serializable,
	Cloneable {
    // don't change this!
    static final long serialVersionUID = -3204386396807876641L;

    /** The maximun allowed length of an atom, in characters */
    public static final int maxAtomLength = 0xff; // one byte length

    private final String atom;

    /**
     * Create an atom from the given string.
     * 
     * @param atom
     *                the string to create the atom from.
     * 
     * @exception java.lang.IllegalArgumentException
     *                    if the string is null or contains more than
     *                    {@link #maxAtomLength maxAtomLength} characters.
     */
    public OtpErlangAtom(final String atom) {
	if (atom == null) {
	    throw new java.lang.IllegalArgumentException(
		    "null string value");
	}

	if (atom.length() > maxAtomLength) {
	    throw new java.lang.IllegalArgumentException("Atom may not exceed "
		    + maxAtomLength + " characters");
	}
	this.atom = atom;
    }

    /**
     * Create an atom from a stream containing an atom encoded in Erlang
     * external format.
     * 
     * @param buf
     *                the stream containing the encoded atom.
     * 
     * @exception OtpErlangDecodeException
     *                    if the buffer does not contain a valid external
     *                    representation of an Erlang atom.
     */
    public OtpErlangAtom(final OtpInputStream buf)
	    throws OtpErlangDecodeException {
	atom = buf.read_atom();
    }

    /**
     * Create an atom whose value is "true" or "false".
     */
    public OtpErlangAtom(final boolean t) {
	atom = String.valueOf(t);
    }

    /**
     * Get the actual string contained in this object.
     * 
     * @return the raw string contained in this object, without regard to Erlang
     *         quoting rules.
     * 
     * @see #toString
     */
    public String atomValue() {
	return atom;
    }

    /**
     * The boolean value of this atom.
     * 
     * @return the value of this atom expressed as a boolean value. If the atom
     *         consists of the characters "true" (independent of case) the value
     *         will be true. For any other values, the value will be false.
     * 
     */
    public boolean booleanValue() {
	return Boolean.valueOf(atomValue()).booleanValue();
    }

    /**
     * Get the printname of the atom represented by this object. The difference
     * between this method and {link #atomValue atomValue()} is that the
     * printname is quoted and escaped where necessary, according to the Erlang
     * rules for atom naming.
     * 
     * @return the printname representation of this atom object.
     * 
     * @see #atomValue
     */
    @Override
    public String toString() {
	if (atomNeedsQuoting(atom)) {
	    return "'" + escapeSpecialChars(atom) + "'";
	} else {
	    return atom;
	}
    }

    /**
     * Determine if two atoms are equal.
     * 
     * @param o
     *                the other object to compare to.
     * 
     * @return true if the atoms are equal, false otherwise.
     */
    @Override
    public boolean equals(final Object o) {

	if (!(o instanceof OtpErlangAtom)) {
	    return false;
	}

	final OtpErlangAtom atom = (OtpErlangAtom) o;
	return this.atom.compareTo(atom.atom) == 0;
    }
    
    @Override
    protected int doHashCode() {
	return atom.hashCode();
    }

    /**
     * Convert this atom to the equivalent Erlang external representation.
     * 
     * @param buf
     *                an output stream to which the encoded atom should be
     *                written.
     */
    @Override
    public void encode(final OtpOutputStream buf) {
	buf.write_atom(atom);
    }

    /* the following four predicates are helpers for the toString() method */
    private boolean isErlangDigit(final char c) {
	return c >= '0' && c <= '9';
    }

    private boolean isErlangUpper(final char c) {
	return c >= 'A' && c <= 'Z' || c == '_';
    }

    private boolean isErlangLower(final char c) {
	return c >= 'a' && c <= 'z';
    }

    private boolean isErlangLetter(final char c) {
	return isErlangLower(c) || isErlangUpper(c);
    }

    // true if the atom should be displayed with quotation marks
    private boolean atomNeedsQuoting(final String s) {
	char c;

	if (s.length() == 0) {
	    return true;
	}
	if (!isErlangLower(s.charAt(0))) {
	    return true;
	}

	final int len = s.length();
	for (int i = 1; i < len; i++) {
	    c = s.charAt(i);

	    if (!isErlangLetter(c) && !isErlangDigit(c) && c != '@') {
		return true;
	    }
	}
	return false;
    }

    /*
     * Get the atom string, with special characters escaped. Note that this
     * function currently does not consider any characters above 127 to be
     * printable.
     */
    private String escapeSpecialChars(final String s) {
	char c;
	final StringBuffer so = new StringBuffer();

	final int len = s.length();
	for (int i = 0; i < len; i++) {
	    c = s.charAt(i);

	    /*
	     * note that some of these escape sequences are unique to Erlang,
	     * which is why the corresponding 'case' values use octal. The
	     * resulting string is, of course, in Erlang format.
	     */

	    switch (c) {
	    // some special escape sequences
	    case '\b':
		so.append("\\b");
		break;

	    case 0177:
		so.append("\\d");
		break;

	    case 033:
		so.append("\\e");
		break;

	    case '\f':
		so.append("\\f");
		break;

	    case '\n':
		so.append("\\n");
		break;

	    case '\r':
		so.append("\\r");
		break;

	    case '\t':
		so.append("\\t");
		break;

	    case 013:
		so.append("\\v");
		break;

	    case '\\':
		so.append("\\\\");
		break;

	    case '\'':
		so.append("\\'");
		break;

	    case '\"':
		so.append("\\\"");
		break;

	    default:
		// some other character classes
		if (c < 027) {
		    // control chars show as "\^@", "\^A" etc
		    so.append("\\^" + (char) ('A' - 1 + c));
		} else if (c > 126) {
		    // 8-bit chars show as \345 \344 \366 etc
		    so.append("\\" + Integer.toOctalString(c));
		} else {
		    // character is printable without modification!
		    so.append(c);
		}
	    }
	}
	return new String(so);
    }

}
