/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.km200.internal;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The KM200Cryption is managing the en- and decription of the communication to the device
 *
 * @author Markus Eckhardt - Initial contribution
 */
public class KM200Cryption {

    private final Logger logger = LoggerFactory.getLogger(KM200Cryption.class);

    private final KM200Device remoteDevice;

    public KM200Cryption(KM200Device remoteDevice) {
        this.remoteDevice = remoteDevice;
    }

    /**
     * This function removes zero padding from a byte array.
     *
     */
    private byte[] removeZeroPadding(byte[] bytes) {
        int i = bytes.length - 1;
        while (i >= 0 && bytes[i] == 0) {
            --i;
        }
        return Arrays.copyOf(bytes, i + 1);
    }

    /**
     * This function adds zero padding to a byte array.
     *
     */
    private byte[] addZeroPadding(byte[] bdata, int bSize, String cSet) throws UnsupportedEncodingException {
        int encryptPadchar = bSize - (bdata.length % bSize);
        byte[] padchars = new String(new char[encryptPadchar]).getBytes(cSet);
        byte[] paddedData = new byte[bdata.length + padchars.length];
        System.arraycopy(bdata, 0, paddedData, 0, bdata.length);
        System.arraycopy(padchars, 0, paddedData, bdata.length, padchars.length);
        return paddedData;
    }

    /**
     * This function does the decoding for a new message from the device
     *
     */
    public String decodeMessage(byte[] encoded) {
        String retString = null;
        byte[] decodedB64 = null;

        // MimeDecoder was the only working decoder.
        decodedB64 = Base64.getMimeDecoder().decode(encoded);

        try {
            /* Check whether the length of the decryptData is NOT multiplies of 16 */
            if ((decodedB64.length & 0xF) != 0) {
                /* Return the data */
                retString = new String(decodedB64, remoteDevice.getCharSet());
                logger.debug("Did NOT decrypt message");
                return retString;
            }
            // --- create cipher
            final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(remoteDevice.getCryptKeyPriv(), "AES"));
            byte[] decryptedData = cipher.doFinal(decodedB64);
            byte[] decryptedDataWOZP = removeZeroPadding(decryptedData);
            return (new String(decryptedDataWOZP, remoteDevice.getCharSet()));
        } catch (UnsupportedEncodingException | GeneralSecurityException e) {
            logger.debug("Exception on encoding", e);
            return null;
        }
    }

    /**
     * This function does the encoding for a new message to the device
     *
     */
    public byte[] encodeMessage(String data) {
        try {
            // --- create cipher
            byte[] bdata = data.getBytes(remoteDevice.getCharSet());
            final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteDevice.getCryptKeyPriv(), "AES"));
            int bsize = cipher.getBlockSize();
            logger.debug("Add Padding, encrypt AES and B64..");
            byte[] encryptedData = cipher.doFinal(addZeroPadding(bdata, bsize, remoteDevice.getCharSet()));
            try {
                return (Base64.getMimeEncoder().encode(encryptedData));
            } catch (IllegalArgumentException e) {
                logger.info("Base64encoding not possible: {}", e.getMessage());
            }
        } catch (UnsupportedEncodingException | GeneralSecurityException e) {
            logger.error("Exception on encoding", e);
        }
        return null;
    }

    /**
     * This function creates the private key from the MD5Salt, the device and the private password
     *
     * @author Markus Eckhardt
     */
    public void recreateKeys() {
        if (StringUtils.isNotBlank(remoteDevice.getGatewayPassword())
                && StringUtils.isNotBlank(remoteDevice.getPrivatePassword()) && remoteDevice.getMD5Salt() != null) {
            byte[] md5K1 = null;
            byte[] md5K2Init = null;
            byte[] md5K2Private = null;
            byte[] bytesOfGatewayPassword = null;
            byte[] bytesOfPrivatePassword = null;

            /* Needed keys for the communication */
            byte[] cryptKeyInit;
            byte[] cryptKeyPriv;
            MessageDigest md = null;
            try {
                md = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                logger.error("No such algorithm, MD5: {}", e.getMessage());
                return;
            }

            /* First half of the key: MD5 of (GatewayPassword . Salt) */
            bytesOfGatewayPassword = remoteDevice.getGatewayPassword().getBytes(StandardCharsets.UTF_8);
            byte[] combParts1 = new byte[bytesOfGatewayPassword.length + remoteDevice.getMD5Salt().length];
            System.arraycopy(bytesOfGatewayPassword, 0, combParts1, 0, bytesOfGatewayPassword.length);
            System.arraycopy(remoteDevice.getMD5Salt(), 0, combParts1, bytesOfGatewayPassword.length,
                    remoteDevice.getMD5Salt().length);
            md5K1 = md.digest(combParts1);

            /* Second half of the key: - Initial: MD5 of ( Salt) */
            md5K2Init = md.digest(remoteDevice.getMD5Salt());

            /* Second half of the key: - private: MD5 of ( Salt . PrivatePassword) */
            bytesOfPrivatePassword = remoteDevice.getPrivatePassword().getBytes(StandardCharsets.UTF_8);
            byte[] combParts2 = new byte[bytesOfPrivatePassword.length + remoteDevice.getMD5Salt().length];
            System.arraycopy(remoteDevice.getMD5Salt(), 0, combParts2, 0, remoteDevice.getMD5Salt().length);
            System.arraycopy(bytesOfPrivatePassword, 0, combParts2, remoteDevice.getMD5Salt().length,
                    bytesOfPrivatePassword.length);
            md5K2Private = md.digest(combParts2);

            /* Create Keys */
            cryptKeyInit = new byte[md5K1.length + md5K2Init.length];
            System.arraycopy(md5K1, 0, cryptKeyInit, 0, md5K1.length);
            System.arraycopy(md5K2Init, 0, cryptKeyInit, md5K1.length, md5K2Init.length);
            remoteDevice.setCryptKeyInit(cryptKeyInit);

            cryptKeyPriv = new byte[md5K1.length + md5K2Private.length];
            System.arraycopy(md5K1, 0, cryptKeyPriv, 0, md5K1.length);
            System.arraycopy(md5K2Private, 0, cryptKeyPriv, md5K1.length, md5K2Private.length);
            remoteDevice.setCryptKeyPriv(cryptKeyPriv);
        }
    }
}
