package org.freertr.ifc;

import org.freertr.addr.addrEmpty;
import org.freertr.addr.addrIP;
import org.freertr.addr.addrIPv4;
import org.freertr.addr.addrIPv6;
import org.freertr.addr.addrMac;
import org.freertr.addr.addrType;
import org.freertr.cfg.cfgIfc;
import org.freertr.ip.ipIfc4;
import org.freertr.ip.ipIfc6;
import org.freertr.pack.packHolder;
import org.freertr.rtr.rtrBgpUtil;
import org.freertr.util.bits;
import org.freertr.util.counter;
import org.freertr.util.debugger;
import org.freertr.util.logger;
import org.freertr.util.state;

/**
 * next hop resolution protocol (rfc2332) handler
 *
 * @author matecsaba
 */
public class ifcNhrp implements ifcUp {

    /**
     * ethertype of these packets
     */
    public final static int ethtyp = 0x2001;

    /**
     * resolution request
     */
    public final static int typResReq = 1;

    /**
     * resolution reply
     */
    public final static int typResRep = 2;

    /**
     * registration request
     */
    public final static int typRegReq = 3;

    /**
     * registration reply
     */
    public final static int typRegRep = 4;

    /**
     * purge request
     */
    public final static int typPurReq = 5;

    /**
     * purge reply
     */
    public final static int typPurRep = 6;

    /**
     * error indication
     */
    public final static int typError = 7;

    /**
     * ipv4 address of server
     */
    public addrIPv4 ip4;

    /**
     * ipv6 address of server
     */
    public addrIPv6 ip6;

    /**
     * time between advertisements
     */
    public int advertiseInterval = 30000;

    /**
     * keepalive
     */
    protected ifcNhrpTxAdv keepTimer;

    private cfgIfc cfg;

    private ifcDn lower = new ifcNull();

    private addrType hwadr = new addrEmpty();

    private counter cntr = new counter();

    /**
     * convert type to string
     *
     * @param typ type
     * @return string
     */
    public static String type2string(int typ) {
        switch (typ) {
            case typResReq:
                return "resReq";
            case typResRep:
                return "resRep";
            case typRegReq:
                return "regReq";
            case typRegRep:
                return "regRep";
            case typPurReq:
                return "purReq";
            case typPurRep:
                return "purRep";
            case typError:
                return "error";
            default:
                return "unknown=" + typ;
        }
    }

    /**
     * create new instance
     *
     * @param ifc interface to use
     */
    public ifcNhrp(cfgIfc ifc) {
        cfg = ifc;
        restartTimer(false);
    }

    public String toString() {
        return "nhrp on " + lower;
    }

    /**
     * received packet
     *
     * @param pck packet
     */
    public void recvPack(packHolder pck) {
        cntr.rx(pck);
        if (pck.msbGetW(0) != ethtyp) {
            cntr.drop(pck, counter.reasons.badEthTyp);
            return;
        }
        pck.getSkip(2);
        int i = pck.msbGetW(10); // packet length
        if (i < 20) {
            cntr.drop(pck, counter.reasons.badSiz);
            return;
        }
        if (pck.dataSize() < i) {
            cntr.drop(pck, counter.reasons.tooSmall);
            return;
        }
        pck.setDataSize(i);
        if (pck.getIPsum(0, i, 0) != 0xffff) {
            cntr.drop(pck, counter.reasons.badSum);
            return;
        }
        if (pck.getByte(16) != 1) { // version
            cntr.drop(pck, counter.reasons.badVer);
            return;
        }
        int typ = pck.getByte(17); // type
        addrType tun;
        switch (pck.getByte(18)) { // nbma size
            case addrIPv4.size:
                tun = new addrIPv4();
                break;
            case addrIPv6.size:
                tun = new addrIPv6();
                break;
            default:
                cntr.drop(pck, counter.reasons.badVal);
                return;
        }
        pck.getSkip(20); // fixed header
        addrType src;
        addrType trg;
        switch (pck.getByte(0)) {
            case addrIPv4.size:
                src = new addrIPv4();
                trg = new addrIPv4();
                break;
            case addrIPv6.size:
                src = new addrIPv6();
                trg = new addrIPv6();
                break;
            default:
                cntr.drop(pck, counter.reasons.badVal);
                return;
        }
        int flg = pck.msbGetW(2); // flags
        int id = pck.msbGetD(4); // id
        pck.getSkip(8); // common header
        pck.getAddr(tun, 0); // source nbma addr
        pck.getSkip(tun.getSize());
        pck.getAddr(src, 0); // source proto addr
        pck.getSkip(src.getSize());
        pck.getAddr(trg, 0); // target proto addr
        pck.getSkip(trg.getSize());
        int cod = pck.getByte(0);
        int tim = pck.msbGetW(6);
        if (debugger.ifcNhrpEvnt) {
            logger.debug("rx typ=" + type2string(typ) + " cod=" + cod + " flg=" + flg + " id=" + id + " tim=" + tim + " tun=" + tun + " src=" + src + " trg=" + trg);
        }
        pck = null;
        switch (typ) {
            case typRegReq:
                pck = createPack(typRegRep, 0, id, 0, tim, tun, src, trg);
                break;
            case typPurReq:
                pck = createPack(typPurRep, 0, id, 0, tim, tun, src, trg);
                break;
            default:
                break;
        }
        if (pck == null) {
            return;
        }
        lower.sendPack(pck);
    }

    /**
     * set parent
     *
     * @param parent parent
     */
    public void setParent(ifcDn parent) {
        lower = parent;
        hwadr = lower.getHwAddr();
    }

    /**
     * set state
     *
     * @param stat state
     */
    public void setState(state.states stat) {
    }

    /**
     * close interface
     */
    public void closeUp() {
    }

    /**
     * get counter
     *
     * @return counter
     */
    public counter getCounter() {
        return cntr;
    }

    /**
     * setup timer thread
     *
     * @param shutdown set true to shut down
     */
    public void restartTimer(boolean shutdown) {
        keepTimer = null;
        if (shutdown) {
            return;
        }
        if (advertiseInterval < 1) {
            return;
        }
        keepTimer = new ifcNhrpTxAdv(this);
        keepTimer.start();
    }

    /**
     * send advertisement
     */
    protected void sendAdvert() {
        addrIP trg = cfg.tunTrg;
        if (trg == null) {
            return;
        }
        if (cfg.tunSrc == null) {
            return;
        }
        addrIP src = cfg.tunSrc.getLocAddr(trg);
        if (src == null) {
            return;
        }
        addrType asrc;
        if (src.isIPv4()) { // nbma
            asrc = src.toIPv4();
        } else {
            asrc = src.toIPv6();
        }
        packHolder pck = createPack(typRegReq, 0, bits.randomD(), 0x8000, (advertiseInterval * 5) / 1000, asrc, cfg.addr4, ip4);
        if (pck != null) {
            lower.sendPack(pck);
        }
        pck = createPack(typRegReq, 0, bits.randomD(), 0x8000, (advertiseInterval * 5) / 1000, asrc, cfg.addr6, ip6);
        if (pck != null) {
            lower.sendPack(pck);
        }
    }

    private packHolder createPack(int typ, int cod, int id, int flg, int tim, addrType nsrc, addrType tsrc, addrType ttrg) {
        if ((tsrc == null) || (ttrg == null)) {
            return null;
        }
        packHolder pck = new packHolder(true, true);
        pck.ETHtrg.fillBytes(0xff);
        if (hwadr.getSize() == addrMac.size) {
            pck.ETHsrc.fromBuf(hwadr.getBytes(), 0);
        }
        pck.msbPutW(0, ethtyp); // ethertype
        pck.putSkip(2);
        pck.msbPutW(0, ((nsrc.getSize() == addrIPv4.size) ? rtrBgpUtil.afiIpv4 : rtrBgpUtil.afiIpv6) >>> 16);
        pck.msbPutW(2, (tsrc.getSize() == addrIPv4.size) ? ipIfc4.type : ipIfc6.type);
        pck.putFill(4, 5, 0); // snap
        pck.putByte(9, 255); // hop count
        pck.msbPutW(10, 0); // packet size
        pck.msbPutW(12, 0); // checksum
        pck.msbPutW(14, 0); // extension offset
        pck.putByte(16, 1); // version
        pck.putByte(17, typ); // type
        pck.putByte(18, nsrc.getSize()); // source address len
        pck.putByte(19, 0); // source subaddress len
        pck.putSkip(20); // fixed part
        pck.putByte(0, tsrc.getSize()); // source proto len
        pck.putByte(1, tsrc.getSize()); // target proto len
        pck.msbPutW(2, flg); // flags
        pck.msbPutD(4, id); // request id
        pck.putSkip(8);
        pck.putAddr(0, nsrc); // nbma
        pck.putSkip(nsrc.getSize());
        pck.putAddr(0, tsrc); // protocol source
        pck.putSkip(tsrc.getSize());
        pck.putAddr(0, ttrg); // protocol target
        pck.putSkip(ttrg.getSize());
        pck.putByte(0, cod); // code
        pck.putByte(1, tsrc.maxBits()); // prefix length
        pck.msbPutW(2, 0); // unused
        pck.msbPutW(4, cfg.ethtyp.getMTUsize()); // mtu
        pck.msbPutW(6, tim);// hold time
        pck.putByte(8, nsrc.getSize()); // source address len
        pck.putByte(9, 0); // source subaddress len
        pck.putByte(10, tsrc.getSize()); // source proto len
        pck.putByte(11, 0); // cie preference
        pck.putSkip(12);
        pck.putAddr(0, nsrc); // nbma
        pck.putSkip(nsrc.getSize());
        pck.putAddr(0, tsrc); // protocol source
        pck.putSkip(tsrc.getSize());
        pck.msbPutD(0, 0x80030000); // responder address
        pck.msbPutD(4, 0x80040000); // forward transit nhs record
        pck.msbPutD(8, 0x80050000); // reverse transit nhs record
        pck.msbPutD(12, 0); // end of list
        pck.putSkip(16);
        int i = pck.headSize() - 2;
        pck.putSkip(-i);
        pck.msbPutW(10, i); // packet size
        pck.msbPutW(14, i - 16); // extension offset
        pck.lsbPutW(12, 0xffff - pck.putIPsum(0, i, 0)); // checksum
        pck.putSkip(i);
        pck.merge2end();
        if (debugger.ifcNhrpEvnt) {
            logger.debug("tx typ=" + type2string(typ) + " cod=" + cod + " flg=" + flg + " id=" + id + " tim=" + tim + " tun=" + nsrc + " src=" + tsrc + " trg=" + ttrg);
        }
        return pck;
    }

}

class ifcNhrpTxAdv implements Runnable {

    private ifcNhrp lower;

    public ifcNhrpTxAdv(ifcNhrp parent) {
        lower = parent;
    }

    public void start() {
        logger.startThread(this);
    }

    public void run() {
        try {
            for (;;) {
                if (lower.keepTimer != this) {
                    break;
                }
                lower.sendAdvert();
                bits.sleep(lower.advertiseInterval);
            }
        } catch (Exception e) {
            logger.traceback(e);
        }
    }

}
