/*
 * %CopyrightBegin%
 *
 * Copyright Ericsson AB 2000-2022. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * %CopyrightEnd%
 */
package com.ericsson.otp.erlang;

/**
 * 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 {
    // don't change this!
    private static final long serialVersionUID = -3204386396807876641L;

    /** The maximum 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.codePointCount(0, atom.length()) > maxAtomLength) {
            throw new java.lang.IllegalArgumentException("Atom may not exceed "
                    + maxAtomLength + " characters: " + atom);
        }
        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) + "'";
        }
        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 other = (OtpErlangAtom) o;
        return atom.compareTo(other.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);
    }

}
