/*******************************************************************************
 * This file is part of OpenNMS(R).
 *
 * Copyright (C) 2011-2012 The OpenNMS Group, Inc.
 * OpenNMS(R) is Copyright (C) 1999-2012 The OpenNMS Group, Inc.
 *
 * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
 *
 * OpenNMS(R) 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 3 of the License,
 * or (at your option) any later version.
 *
 * OpenNMS(R) 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 OpenNMS(R).  If not, see:
 *      http://www.gnu.org/licenses/
 *
 * For more information contact:
 *     OpenNMS(R) Licensing <license@opennms.org>
 *     http://www.opennms.org/
 *     http://www.opennms.com/
 *******************************************************************************/

package org.opennms.protocols.snmp;

import java.io.Serializable;

import org.opennms.protocols.snmp.asn1.AsnDecodingException;
import org.opennms.protocols.snmp.asn1.AsnEncoder;
import org.opennms.protocols.snmp.asn1.AsnEncodingException;

/**
 * Implements the ASN1.UNIVERSAL Octet String datatype. The string is a sequence
 * of 8-bit octet data. The format of the 8-bit characters are defined by the
 * application.
 * 
 * @author <a href="mailto:weave@oculan.com>Brian Weaver </a>
 */
public class SnmpOctetString extends Object implements SnmpSyntax, Cloneable, Serializable {
    /**
     * Required to allow evolution of serialization format.
     */
    static final long serialVersionUID = 1848780739976444105L; // generated by
                                                                // serialver
                                                                // tool

    /**
     * The actual octet string data (UTF-8)
     * 
     */
    private byte[] m_data;

    /**
     * The ASN.1 value for the OCTET STRING type.
     * 
     */
    public static final byte ASNTYPE = SnmpSMI.SMI_STRING;

    /**
     * This can be used by a derived class to <em>force</em> the data
     * contained by the octet string. The data is not duplicated, only the
     * reference to the array is stored. No validation of data is performed at
     * all.
     * 
     * @param data
     *            The new data buffer.
     */
    protected void assumeString(final byte[] data) {
        m_data = data == null? data : data.clone();
    }

    /**
     * The default class constructor. Constructs an Octet String with a length
     * of zero and no data.
     * 
     */
    public SnmpOctetString() {
        m_data = null;
    }

    /**
     * Constructs an octet string with the inital value equal to data. The data
     * is actually copied so changes to the data reference do not affect the
     * Octet string object.
     * 
     * @param data
     *            The data to be copied to self
     * 
     */
    public SnmpOctetString(byte[] data) {
        this();
        if (data != null) {
            m_data = new byte[data.length];
            System.arraycopy(data, 0, m_data, 0, data.length);
        }
    }

    /**
     * Class copy constructor. Constructs and octet string object that is a
     * duplicate of the object second.
     * 
     * @param second
     *            The object to copy into self
     * 
     */
    public SnmpOctetString(SnmpOctetString second) {
        this(second.m_data);
    }

    /**
     * Returns a reference to the internal object string. Changes to this byte
     * array WILL affect the octet string object. These changes should not be
     * made lightly.
     * 
     * @return A reference to the internal byte array.
     */
    public byte[] getString() {
        return m_data;
    }

    /**
     * Sets the internal string array so that it is identical to the passed
     * array. The array is actually copied so that changes to data after the
     * construction of the object are not reflected in the SnmpOctetString
     * Object.
     * 
     * @param data
     *            The new octet string data.
     * 
     */
    public void setString(byte[] data) {
        m_data = null;
        if (data != null) {
            m_data = new byte[data.length];
            System.arraycopy(data, 0, m_data, 0, data.length);
        }
    }

    /**
     * Sets the internal octet string equal to the converted stirng via the
     * method getBytes(). This may cause some data corruption since the
     * conversion is platform specific.
     * 
     * @param data
     *            The new octet string data.
     * 
     * @see java.lang.String#getBytes()
     */
    public void setString(String data) {
        m_data = null;
        if (data != null) {
            m_data = data.getBytes();
        }
    }

    /**
     * Returns the internal length of the octet string. This method is favored
     * over recovereing the length from the internal array. The method
     * compensates for a null set of data and returns zero if the internal array
     * is null.
     * 
     * @return The length of the octet string.
     * 
     */
    public int getLength() {
        int len = 0;
        if (m_data != null)
            len = m_data.length;
        return len;
    }

    /**
     * Returns the ASN.1 type identifier for the Octet String.
     * 
     * @return The ASN.1 identifier.
     * 
     */
    @Override
    public byte typeId() {
        return ASNTYPE;
    }

    /**
     * Encodes the ASN.1 octet string using the passed encoder and stores the
     * results in the passed buffer. An exception is thrown if an error occurs
     * with the encoding of the information.
     * 
     * @param buf
     *            The buffer to write the encoded information.
     * @param offset
     *            The offset to start writing information
     * @param encoder
     *            The encoder object.
     * 
     * @return The offset of the byte immediantly after the last encoded byte.
     * 
     * @exception AsnEncodingException
     *                Thrown if the encoder finds an error in the buffer.
     */
    @Override
    public int encodeASN(byte[] buf, int offset, AsnEncoder encoder) throws AsnEncodingException {
        if (m_data == null)
            throw new AsnEncodingException("No data in octet string");

        return encoder.buildString(buf, offset, typeId(), m_data);
    }

    /**
     * Decodes the ASN.1 octet string from the passed buffer. If an error occurs
     * during the decoding sequence then an AsnDecodingException is thrown by
     * the method. The value is decoded using the AsnEncoder passed to the
     * object.
     * 
     * @param buf
     *            The encode buffer
     * @param offset
     *            The offset byte to begin decoding
     * @param encoder
     *            The decoder object.
     * 
     * @return The index of the byte immediantly after the last decoded byte of
     *         information.
     * 
     * @exception AsnDecodingException
     *                Thrown by the encoder if an error occurs trying to decode
     *                the data buffer.
     */
    @Override
    public int decodeASN(byte[] buf, int offset, AsnEncoder encoder) throws AsnDecodingException {
        Object[] rVals = encoder.parseString(buf, offset);

        if (((Byte) rVals[1]).byteValue() != typeId())
            throw new AsnDecodingException("Invalid ASN.1 type");

        m_data = (byte[]) rVals[2];

        return ((Integer) rVals[0]).intValue();
    }

    /**
     * Creates a duplicate copy of the object and returns it to the caller.
     * 
     * @return A newly constructed copy of self
     * 
     */
    @Override
    public SnmpSyntax duplicate() {
        return new SnmpOctetString(this);
    }

    /**
     * Creates a duplicate copy of the object and returns it to the caller.
     * 
     * @return A newly constructed copy of self
     * 
     */
    @Override
    public Object clone() {
        return new SnmpOctetString(this);
    }

    /**
     * Returns a string representation of the object. If the object contains
     * non-printable characters then the contents are printed in hexidecimal.
     * 
     */
    @Override
    public String toString() {
        //
        // check for non-printable characters. If they
        // exist then print the string out as hexidecimal
        //
        boolean asHex = false;
        for (int i = 0; i < m_data.length; i++) {
            byte b = m_data[i];
            if ((b < 32 && b != 10 && b != 13) || b == 127) {
                asHex = true;
                break;
            }
        }

        String rs = null;
        if (asHex) {
            //
            // format the string for hex
            //
            StringBuffer b = new StringBuffer();
            // b.append("SNMP Octet String [length = " + m_data.length + ", fmt
            // = HEX] = [");
            for (int i = 0; i < m_data.length; ++i) {
                int x = (int) m_data[i] & 0xff;
                if (x < 16)
                    b.append('0');
                b.append(Integer.toString(x, 16).toUpperCase());

                if (i < m_data.length - 1)
                    b.append(' ');
            }
            // b.append(']');
            rs = b.toString();
        } else {
            //
            // raw output
            //
            // rs = "SNMP Octet String [length = " + m_data.length + ", fmt =
            // RAW] = [" + new String(m_data) + "]";
            rs = new String(m_data);
        }

        return rs;
    }

    /**
     * This method takes an SnmpOctetString and replaces any unprintable
     * characters with ASCII period ('.') and returns the resulting character
     * string. Special case in which the supplied SnmpOctetString consists of a
     * single ASCII Null byte is also handled. In this special case an empty
     * string is returned.
     * 
     * NOTE: A character is considered unprintable if its decimal value falls
     * outside of the range: 32 - 126.
     * 
     * @param octetString
     *            SnmpOctetString from which to generate the String
     * 
     * @return a Java String object created from the octet string's byte array.
     */
    public static String toDisplayString(SnmpOctetString octetString) {
        // TODO: Move this to a more common place
        // Valid SnmpOctetString object
        if (octetString == null) {
            return null;
        }
    
        byte bytes[] = octetString.getString();
    
        // Sanity check
        if (bytes == null || bytes.length == 0) {
            return null;
        }
    
        // Check for special case where byte array contains a single
        // ASCII null character
        if (bytes.length == 1 && bytes[0] == 0) {
            return null;
        }
    
        // Replace all unprintable chars (chars outside of
        // decimal range 32 - 126 inclusive) with an
        // ASCII period char (decimal 46).
        // 
        byte newBytes[] = new byte[bytes.length];
        for (int i = 0; i < bytes.length; i++) {
            newBytes[i] = Character.isISOControl((char)bytes[i]) ? (byte)'.' : bytes[i];
        }
    
        // Create string, trim any white-space and return
        String result = new String(newBytes);
        return result.trim();
    }

    // TODO: Move this to common base class
    public static String toHexString(final SnmpOctetString ostr) {
        if (ostr == null) return null;
        StringBuffer sbuf = new StringBuffer();
        if (ostr.getLength() > 0) {
            byte[] bytes = ostr.getString();
            for (int i = 0; i < bytes.length; i++) {
                sbuf.append(Integer.toHexString(((int) bytes[i] >> 4) & 0xf));
                sbuf.append(Integer.toHexString((int) bytes[i] & 0xf));
            }
        }
        String physAddr = sbuf.toString().trim();
        return physAddr;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof SnmpOctetString) {
            SnmpOctetString str = (SnmpOctetString)obj;
            if (m_data == null) return (str.m_data == null);
            
            if (m_data.length != str.m_data.length) return false;
            
            for(int i = 0; i < m_data.length; i++)
                if (m_data[i] != str.m_data[i])
                    return false;
            
            return true;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return 0;
    }
}
