/*
 * Copyright 2016 Open Networking Laboratory
 *
 * 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.
 */
package org.onosproject.ospf.protocol.util;

import org.onosproject.ospf.controller.OspfLsa;
import org.onosproject.ospf.controller.OspfLsaType;
import org.onosproject.ospf.protocol.lsa.types.AsbrSummaryLsa;
import org.onosproject.ospf.protocol.lsa.types.ExternalLsa;
import org.onosproject.ospf.protocol.lsa.types.NetworkLsa;
import org.onosproject.ospf.protocol.lsa.types.OpaqueLsa10;
import org.onosproject.ospf.protocol.lsa.types.OpaqueLsa11;
import org.onosproject.ospf.protocol.lsa.types.OpaqueLsa9;
import org.onosproject.ospf.protocol.lsa.types.RouterLsa;
import org.onosproject.ospf.protocol.lsa.types.SummaryLsa;
import org.onosproject.ospf.protocol.ospfpacket.OspfMessage;
import org.onosproject.ospf.protocol.ospfpacket.types.DdPacket;
import org.onosproject.ospf.protocol.ospfpacket.types.HelloPacket;
import org.onosproject.ospf.protocol.ospfpacket.types.LsAcknowledge;
import org.onosproject.ospf.protocol.ospfpacket.types.LsRequest;
import org.onosproject.ospf.protocol.ospfpacket.types.LsUpdate;

import java.util.Arrays;

/**
 * Calculates checksum for different types of OSPF packets.
 */
public class ChecksumCalculator {

    /**
     * Converts given string to sixteen bits integer.
     * If hexasum is more than 16 bit value, needs to be reduced to 16 bit value.
     *
     * @param strToConvert hexasum value to convert
     * @return 16 bit integer value
     */
    public static int convertToSixteenBits(String strToConvert) {
        StringBuilder sb = new StringBuilder(strToConvert);
        sb = sb.reverse();
        StringBuilder s1 = new StringBuilder(sb.substring(0, 4));
        s1 = s1.reverse();
        StringBuilder s2 = new StringBuilder(sb.substring(4, sb.length()));
        s2 = s2.reverse();
        int num = Integer.parseInt(s1.toString(), 16) + Integer.parseInt(s2.toString(), 16);
        return num;
    }

    /**
     * Checks whether checksum is valid or not in the given OSPF message.
     *
     * @param ospfMessage  ospf message instance
     * @param checksumPos1 position of checksum bit in packet
     * @param checksumPos2 position of checksum bit in packet
     * @return true if valid else false
     */
    public boolean isValidOspfCheckSum(OspfMessage ospfMessage, int checksumPos1, int checksumPos2) {

        switch (ospfMessage.ospfMessageType().value()) {
            case OspfParameters.HELLO:
                ospfMessage = (HelloPacket) ospfMessage;
                break;
            case OspfParameters.DD:
                ospfMessage = (DdPacket) ospfMessage;
                break;
            case OspfParameters.LSREQUEST:
                ospfMessage = (LsRequest) ospfMessage;
                break;
            case OspfParameters.LSUPDATE:
                ospfMessage = (LsUpdate) ospfMessage;
                break;
            case OspfParameters.LSACK:
                ospfMessage = (LsAcknowledge) ospfMessage;
                break;
            default:
                break;
        }

        byte[] messageAsBytes = ospfMessage.asBytes();
        return validateOspfCheckSum(messageAsBytes, checksumPos1, checksumPos2);
    }

    /**
     * Checks whether checksum is valid or not in the given OSPF LSA.
     *
     * @param ospfLsa         lsa instance
     * @param lsType          lsa type
     * @param lsaChecksumPos1 lsa checksum position in packet
     * @param lsaChecksumPos2 lsa checksum position in packet
     * @return true if valid else false
     * @throws Exception might throw exception while processing
     */
    public boolean isValidLsaCheckSum(OspfLsa ospfLsa, int lsType, int lsaChecksumPos1,
                                      int lsaChecksumPos2) throws Exception {
        if (lsType == OspfLsaType.ROUTER.value()) {
            RouterLsa lsa = (RouterLsa) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.NETWORK.value()) {
            NetworkLsa lsa = (NetworkLsa) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.SUMMARY.value()) {
            SummaryLsa lsa = (SummaryLsa) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.ASBR_SUMMARY.value()) {
            AsbrSummaryLsa lsa = (AsbrSummaryLsa) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.EXTERNAL_LSA.value()) {
            ExternalLsa lsa = (ExternalLsa) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.LINK_LOCAL_OPAQUE_LSA.value()) {
            OpaqueLsa9 lsa = (OpaqueLsa9) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.AREA_LOCAL_OPAQUE_LSA.value()) {
            OpaqueLsa10 lsa = (OpaqueLsa10) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        } else if (lsType == OspfLsaType.AS_OPAQUE_LSA.value()) {
            OpaqueLsa11 lsa = (OpaqueLsa11) ospfLsa;
            return validateLsaCheckSum(lsa.asBytes(), lsaChecksumPos1, lsaChecksumPos2);
        }

        return false;
    }

    /**
     * Verifies the checksum is valid in given LSA packet bytes.
     *
     * @param lsaPacket       lsa as byte array
     * @param lsaChecksumPos1 position of checksum bit in packet
     * @param lsaChecksumPos2 position of checksum bit in packet
     * @return true if valid else false
     */
    public boolean validateLsaCheckSum(byte[] lsaPacket, int lsaChecksumPos1, int lsaChecksumPos2) {

        byte[] checksum = calculateLsaChecksum(lsaPacket, lsaChecksumPos1, lsaChecksumPos2);

        if (lsaPacket[lsaChecksumPos1] == checksum[0] && lsaPacket[lsaChecksumPos2] == checksum[1]) {
            return true;
        }

        return false;
    }

    /**
     * Verifies the checksum is valid in given OSPF packet bytes.
     *
     * @param ospfPacket   as byte array
     * @param checksumPos1 position of checksum bit in packet
     * @param checksumPos2 position of checksum bit in packet
     * @return true if valid else false
     */
    public boolean validateOspfCheckSum(byte[] ospfPacket, int checksumPos1, int checksumPos2) {

        byte[] checkSum = calculateOspfCheckSum(ospfPacket, checksumPos1, checksumPos2);

        if (ospfPacket[checksumPos1] == checkSum[0] && ospfPacket[checksumPos2] == checkSum[1]) {
            return true;
        }

        return false;
    }

    /**
     * Calculates the LSA checksum.
     *
     * @param lsaBytes        as byte array
     * @param lsaChecksumPos1 position of checksum bit in packet
     * @param lsaChecksumPos2 position of checksum bit in packet
     * @return checksum bytes
     */
    public byte[] calculateLsaChecksum(byte[] lsaBytes, int lsaChecksumPos1, int lsaChecksumPos2) {

        byte[] tempLsaByte = Arrays.copyOf(lsaBytes, lsaBytes.length);

        int[] checksumOut = {0, 0};
        tempLsaByte[lsaChecksumPos1] = 0;
        tempLsaByte[lsaChecksumPos2] = 0;
        byte[] byteCheckSum = {0, 0};
        if (lsaBytes != null) {
            for (int i = 2; i < tempLsaByte.length; i++) {
                checksumOut[0] = checksumOut[0] + ((int) tempLsaByte[i] & 0xFF);
                checksumOut[1] = checksumOut[1] + checksumOut[0];
            }
            checksumOut[0] = checksumOut[0] % 255;
            checksumOut[1] = checksumOut[1] % 255;
        }
        int byte1 = (int) ((tempLsaByte.length - lsaChecksumPos1 - 1) * checksumOut[0] - checksumOut[1]) % 255;
        if (byte1 <= 0) {
            byte1 += 255;
        }
        int byte2 = 510 - checksumOut[0] - byte1;
        if (byte2 > 255) {
            byte2 -= 255;
        }

        byteCheckSum[0] = (byte) byte1;
        byteCheckSum[1] = (byte) byte2;

        return byteCheckSum;
    }

    /**
     * Calculate checksum from hexasum.
     *
     * @param hexasum total of 16 bits hexadecimal values
     * @return checksum value
     */
    private int calculateChecksum(int hexasum) {

        char[] tempZeros = {'0', '0', '0', '0'};
        StringBuffer hexaAsBinaryStr = new StringBuffer(Integer.toBinaryString(hexasum));
        int length = hexaAsBinaryStr.length();
        while (length > 16) {
            if (hexaAsBinaryStr.length() % 4 != 0) {
                int offset = hexaAsBinaryStr.length() % 4;
                hexaAsBinaryStr.insert(0, tempZeros, 0, 4 - offset);
            }
            StringBuffer hexaStr1 = new StringBuffer(hexaAsBinaryStr.reverse().substring(0, 16));
            String revHexaStr1 = hexaStr1.reverse().toString();
            StringBuffer hexaStr2 = new StringBuffer(hexaAsBinaryStr.reverse());
            StringBuffer hexaStr3 = new StringBuffer(hexaStr2.reverse().substring(16, hexaStr2.length()));
            String revHexaStr3 = hexaStr3.reverse().toString();
            int lastSixteenHexaBits = Integer.parseInt(revHexaStr1, 2);
            int remainingHexaBits = Integer.parseInt(revHexaStr3, 2);
            int totalCheckSum = lastSixteenHexaBits + remainingHexaBits;
            hexaAsBinaryStr = new StringBuffer(Integer.toBinaryString(totalCheckSum));
            length = hexaAsBinaryStr.length();
        }
        if (hexaAsBinaryStr.length() < 16) {
            int count = 16 - hexaAsBinaryStr.length();
            String s = hexaAsBinaryStr.toString();
            for (int i = 0; i < count; i++) {
                s = "0" + s;
            }

            hexaAsBinaryStr = new StringBuffer(s);

        }
        StringBuffer checksum = negate(hexaAsBinaryStr);
        return Integer.parseInt(checksum.toString(), 2);
    }

    /**
     * Negates given hexasum.
     *
     * @param binaryString binary form of hexasum
     * @return binary from of calculateChecksum
     */
    private StringBuffer negate(StringBuffer binaryString) {
        for (int i = 0; i < binaryString.length(); i++) {
            if (binaryString.charAt(i) == '1') {
                binaryString.replace(i, i + 1, "0");
            } else {
                binaryString.replace(i, i + 1, "1");
            }
        }

        return binaryString;
    }

    /**
     * Calculates the OSPF checksum for the given packet.
     *
     * @param packet       as byte array
     * @param checksumPos1 position of checksum bit in packet
     * @param checksumPos2 position of checksum bit in packet
     * @return checksum bytes
     */
    public byte[] calculateOspfCheckSum(byte[] packet, int checksumPos1, int checksumPos2) {

        int hexasum = 0;
        for (int i = 0; i < packet.length; i = i + 2) {
            if (i != 12) {
                byte b1 = packet[i];
                String s1 = String.format("%8s", Integer.toBinaryString(b1 & 0xFF)).replace(' ', '0');
                b1 = packet[i + 1];
                String s2 = String.format("%8s", Integer.toBinaryString(b1 & 0xFF)).replace(' ', '0');
                String hexa = s1 + s2;
                int num1 = Integer.parseInt(hexa, 2);
                hexasum = hexasum + num1;
                String convertTo16 = Integer.toHexString(hexasum);
                if (convertTo16.length() > 4) {
                    hexasum = convertToSixteenBits(convertTo16);
                }
            }
        }
        StringBuilder sb = new StringBuilder(Integer.toHexString(hexasum));
        if (sb.length() > 4) {
            sb = sb.reverse();
            StringBuilder s1 = new StringBuilder(sb.substring(0, 4));
            s1 = s1.reverse();
            StringBuilder s2 = new StringBuilder(sb.substring(4, sb.length()));
            s2 = s2.reverse();
            hexasum = Integer.parseInt(s1.toString(), 16) + Integer.parseInt(s2.toString(), 16);
        }
        int finalChecksum = calculateChecksum(hexasum);
        return OspfUtil.convertToTwoBytes(finalChecksum);
    }
}