/*
 * Copyright 2006-2010 Daniel Henninger.  All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package net.sf.kraken.avatars;

import org.apache.log4j.Logger;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.util.Base64;
import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.StringUtils;
import org.xmpp.packet.JID;

import java.io.ByteArrayInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;

/**
 * @author Daniel Henninger
 */
public class Avatar {

    static Logger Log = Logger.getLogger(Avatar.class);

    private static final String INSERT_AVATAR =
            "INSERT INTO ofGatewayAvatars(jid, xmppHash, legacyIdentifier, createDate, lastUpdate, imageType, imageData) " +
            "VALUES (?,?,?,?,?,?,?)";
    private static final String DELETE_AVATAR =
            "DELETE FROM ofGatewayAvatars WHERE jid=?";
    private static final String LOAD_AVATAR =
            "SELECT xmppHash, legacyIdentifier, createDate, lastUpdate, imageType " +
            "FROM ofGatewayAvatars WHERE jid=?";
    private static final String RETRIEVE_IMAGE =
            "SELECT imageData FROM ofGatewayAvatars WHERE jid=?";
    private static final String UPDATE_LEGACY_ID =
            "UPDATE ofGatewayAvatars SET legacyIdentifier=? WHERE jid=?";

    private JID jid;
    private String xmppHash;
    private String legacyIdentifier;
    private Date createDate;
    private Date lastUpdate;
    private String mimeType;

    /**
     * Creates a new avatar entry.
     *
     * @param jid JID of the avatar.
     * @param imageData Binary image data.
     * @throws IllegalArgumentException if any of the arguments are null.
     */
    public Avatar(JID jid, byte[] imageData) throws IllegalArgumentException {
        if (jid == null || imageData == null) {
            throw new IllegalArgumentException("Avatar: Passed null argument.");
        }
        this.jid = jid;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(imageData);
            this.xmppHash = StringUtils.encodeHex(md.digest());
        }
        catch (NoSuchAlgorithmException e) {
            Log.error("Avatar: Unable to find support for SHA algorithm?");
        }
        this.createDate = new Date();
        this.lastUpdate = new Date();
        ImageInfo imageInfo = new ImageInfo();
        imageInfo.setInput(new ByteArrayInputStream(imageData));
        this.mimeType = imageInfo.getMimeType();

        try {
            insertIntoDb(Base64.encodeBytes(imageData));
        }
        catch (SQLException e) {
            Log.error("Avatar: SQL exception while inserting avatar: ", e);
        }
    }

    /**
     * Creates a new avatar entry.
     *
     * @param jid JID of the avatar.
     * @param legacyIdentifier Hash or whatever is necessary to identify the avatar on the legacy network.
     * @param imageData Binary image data.
     * @throws IllegalArgumentException if any of the arguments are null.
     */
    public Avatar(JID jid, String legacyIdentifier, byte[] imageData) throws IllegalArgumentException {
        if (jid == null || legacyIdentifier == null || imageData == null) {
            throw new IllegalArgumentException("Avatar: Passed null argument.");
        }
        this.jid = jid;
        this.legacyIdentifier = legacyIdentifier;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(imageData);
            this.xmppHash = StringUtils.encodeHex(md.digest());
        }
        catch (NoSuchAlgorithmException e) {
            Log.error("Avatar: Unable to find support for SHA algorithm?");
        }
        this.createDate = new Date();
        this.lastUpdate = new Date();
        ImageInfo imageInfo = new ImageInfo();
        imageInfo.setInput(new ByteArrayInputStream(imageData));
        this.mimeType = imageInfo.getMimeType();
        try {
            insertIntoDb(Base64.encodeBytes(imageData));
        }
        catch (SQLException e) {
            Log.error("Avatar: SQL exception while inserting avatar: ", e);
        }
    }

    /**
     * Loads an existing avatar.
     *
     * @param jid JID of the contact whose avatar we are retrieving.
     * @throws NotFoundException if avatar entry was not found in database.
     */
    public Avatar(JID jid) throws NotFoundException {
        this.jid = jid;
        loadFromDb();
        Log.debug("Loaded avatar for "+this.jid+" of hash "+this.xmppHash);
    }

    /**
     * Returns the JID of the avatar.
     *
     * @return JID of avatar.
     */
    public JID getJid() {
        return jid;
    }

    /**
     * Returns the XMPP hash (sha1).
     *
     * @return SHA1 based XMPP hash.
     */
    public String getXmppHash() {
        return xmppHash;
    }

    /**
     * Returns the legacy identifier of the avatar.
     *
     * This is completely up to the transport and is generally whatever type of hash is used on their end.
     *
     * @return Legacy identifier for the avatar.
     */
    public String getLegacyIdentifier() {
        return legacyIdentifier;
    }

    /**
     * Sets the legacy identifier of the avatar.
     *
     * This is typically used for setting your own avatar information on the legacy service,
     * and called after the avatar is set to store the known legacy identifier, if there is one,
     * associated with your avatar.
     *
     * @param identifier Identifier to store.
     */
    public void setLegacyIdentifier(String identifier) {
        this.legacyIdentifier = identifier;
        Connection con = null;
        PreparedStatement pstmt = null;
        boolean abortTransaction = false;
        try {
            con = DbConnectionManager.getTransactionConnection();
            pstmt = con.prepareStatement(UPDATE_LEGACY_ID);
            pstmt.setString(1, jid.toString());
            pstmt.setString(2, legacyIdentifier);
            pstmt.executeUpdate();
        }
        catch (SQLException sqle) {
            abortTransaction = true;
            Log.error("Avatar: Major SQL error while updating legacy identifier: ", sqle);
        }
        finally {
            DbConnectionManager.closeTransactionConnection(pstmt, con, abortTransaction);
        }
    }

    /**
     * Returns the creation date of the avatar in the database.
     *
     * @return Creation date of the avatar.
     */
    public Date getCreateDate() {
        return createDate;
    }

    /**
     * Returns the date the avatar was last updated.
     *
     * @return Last update date of the avatar.
     */
    public Date getLastUpdate() {
        return lastUpdate;
    }

    /**
     * Returns the mime type of the image that is stored.
     *
     * @return Mime type of the avatar image.
     */
    public String getMimeType() {
        return mimeType;
    }

    /**
     * Retrieves the actual image data for this avatar.
     *
     * @return The base64 encoded image data for the avatar.
     * @throws NotFoundException if avatar entry was not found in database.
     */
    public String getImageData() throws NotFoundException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        String imageData = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(RETRIEVE_IMAGE);
            pstmt.setString(1, jid.toString());
            rs = pstmt.executeQuery();
            if (!rs.next()) {
                throw new NotFoundException("Avatar not found for " + jid);
            }
            imageData = rs.getString(1);
        }
        catch (SQLException sqle) {
            Log.error(sqle);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return imageData;
    }

    /**
     * Inserts a new avaar into the database.
     *
     * @param imageData Base64 encoded image data to be stored in database.
     * @throws SQLException if the SQL statement is wrong for whatever reason.
     */
    private void insertIntoDb(String imageData) throws SQLException {
        Connection con = null;
        PreparedStatement pstmt = null;
        boolean abortTransaction = false;
        synchronized ("avatar"+jid.toString()) {
            try {
                con = DbConnectionManager.getTransactionConnection();
                pstmt = con.prepareStatement(DELETE_AVATAR);
                pstmt.setString(1, jid.toString());
                pstmt.executeUpdate();
                pstmt = con.prepareStatement(INSERT_AVATAR);
                pstmt.setString(1, jid.toString());
                pstmt.setString(2, xmppHash);
                pstmt.setString(3, legacyIdentifier);
                pstmt.setLong(4, createDate.getTime());
                pstmt.setLong(5, lastUpdate.getTime());
                pstmt.setString(6, mimeType);
                pstmt.setString(7, imageData);
                pstmt.executeUpdate();
            }
            catch (SQLException sqle) {
                abortTransaction = true;
                throw sqle;
            }
            finally {
                DbConnectionManager.closeTransactionConnection(pstmt, con, abortTransaction);
            }
        }
    }

    /**
     * Load avatar from database.
     *
     * @throws NotFoundException if avatar entry was not found in database.
     */
    private void loadFromDb() throws NotFoundException {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_AVATAR);
            pstmt.setString(1, jid.toString());
            rs = pstmt.executeQuery();
            if (!rs.next()) {
                throw new NotFoundException("Avatar not found for " + jid);
            }
            this.xmppHash = rs.getString(1);
            this.legacyIdentifier = rs.getString(2);
            this.createDate = new Date(rs.getLong(3));
            this.lastUpdate = new Date(rs.getLong(4));
            this.mimeType = rs.getString(5);
        }
        catch (SQLException sqle) {
            Log.error(sqle);
        }
        finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
    }

}
