package org.freertr.ifc;

import org.freertr.addr.addrEmpty;
import org.freertr.addr.addrMac;
import org.freertr.addr.addrType;
import org.freertr.cfg.cfgAll;
import org.freertr.cfg.cfgIfc;
import org.freertr.pack.packHolder;
import org.freertr.pack.packPppOE;
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;
import org.freertr.enc.encTlv;
import org.freertr.user.userFormat;

/**
 * ppp over ethernet (rfc2516) protocol relay handler
 *
 * @author matecsaba
 */
public class ifcP2pOErely implements ifcUp {

    /**
     * counter of this interface
     */
    public counter cntr = new counter();

    /**
     * server that sends our packets
     */
    public ifcDn lower = new ifcNull();

    /**
     * configured service name
     */
    public String serviceNam = "pppoe";

    /**
     * configured service delay
     */
    public int serviceDly = 0;

    /**
     * interface to clone
     */
    public cfgIfc clnIfc;

    /**
     * client address
     */
    public addrMac clntAdr = new addrMac();

    /**
     * client session
     */
    public int clntSes = -1;

    /**
     * hardware address
     */
    public addrMac hwaddr;

    /**
     * serial handler
     */
    public final ifcP2pOErelySer ser;

    /**
     * dialer handler
     */
    public final ifcP2pOErelyDial dia;

    /**
     * dialer interface handler
     */
    public final ifcDn diaI;

    /**
     * get show
     *
     * @param l list to append
     */
    public void getShow(userFormat l) {
        l.add(clntAdr + "|" + clntSes + "|" + clnIfc.name);
    }

    public String toString() {
        String a = "";
        if (!serviceNam.equals("pppoe")) {
            a += " name " + serviceNam;
        }
        if (serviceDly > 0) {
            a += " delay " + serviceDly;
        }
        return clnIfc.name + a;
    }

    /**
     * create new instance
     *
     * @param mod true for serial, false for dialer
     */
    public ifcP2pOErely(boolean mod) {
        if (mod) {
            ser = new ifcP2pOErelySer(this);
            dia = null;
            diaI = null;
        } else {
            dia = new ifcP2pOErelyDial(this);
            ser = null;
            diaI = dia;
        }
    }

    public counter getCounter() {
        return cntr;
    }

    public void setState(state.states stat) {
    }

    public void closeUp() {
    }

    public void setParent(ifcDn parent) {
        lower = parent;
        hwaddr = (addrMac) lower.getHwAddr();
    }

    public void recvPack(packHolder pck) {
        packPppOE poe = new packPppOE();
        if (poe.parseHeader(pck)) {
            cntr.drop(pck, counter.reasons.badHdr);
            return;
        }
        if (poe.cod == packPppOE.codeData) {
            if (clntSes != poe.ses) {
                cntr.drop(pck, counter.reasons.badVal);
                return;
            }
            if (clntAdr.compareTo(pck.ETHsrc) != 0) {
                cntr.drop(pck, counter.reasons.badAddr);
                return;
            }
            pck.putStart();
            pck.msbPutW(0, ifcPpp.preamble);
            pck.putSkip(2);
            pck.merge2beg();
            if (ser != null) {
                ser.lower.sendPack(pck);
            } else {
                dia.upper.recvPack(pck);
            }
            return;
        }
        if (debugger.ifcP2pOErely) {
            logger.debug("rx " + packPppOE.code2string(poe.cod) + " sess=" + poe.ses);
        }
        encTlv tlv = new encTlv(packPppOE.tlv);
        byte[] host = null;
        for (;;) {
            if (tlv.getBytes(pck)) {
                break;
            }
            if (tlv.valTyp == packPppOE.typeEol) {
                break;
            }
            switch (tlv.valTyp) {
                case packPppOE.typeHstUnq:
                    host = tlv.copyBytes();
                    break;
                case packPppOE.typeEol:
                case packPppOE.typeSrvNam:
                case packPppOE.typeACnam:
                case packPppOE.typeACcok:
                case packPppOE.typeRlySes:
                case packPppOE.typeVndSpc:
                case packPppOE.typeSrvNm:
                case packPppOE.typeSysErr:
                case packPppOE.typeGenErr:
                    break;
            }
        }
        addrMac src = pck.ETHsrc.copyBytes();
        pck.clear();
        pck.putStart();
        pck.ETHtrg.setAddr(src);
        pck.ETHsrc.setAddr(hwaddr);
        if (host != null) {
            tlv.putBytes(pck, packPppOE.typeHstUnq, host);
        }
        tlv.putStr(pck, packPppOE.typeACnam, cfgAll.hostName);
        tlv.putStr(pck, packPppOE.typeSrvNam, serviceNam);
        switch (poe.cod) {
            case packPppOE.codePadI:
                if (debugger.ifcP2pOErely) {
                    logger.debug("tx pado");
                }
                packPppOE.updateHeader(pck, packPppOE.codePadO, 0);
                ifcDelay.sendPack(serviceDly, lower, pck);
                break;
            case packPppOE.codePadR:
                if (debugger.ifcP2pOErely) {
                    logger.debug("tx pads");
                }
                clntSes = bits.random(1, 0xfffe);
                clntAdr = src;
                packPppOE.updateHeader(pck, packPppOE.codePadS, clntSes);
                lower.sendPack(pck);
                break;
            case packPppOE.codePadT:
                if (clntSes != poe.ses) {
                    cntr.drop(pck, counter.reasons.badVal);
                    return;
                }
                if (clntAdr.compareTo(src) != 0) {
                    cntr.drop(pck, counter.reasons.badAddr);
                    return;
                }
                clntAdr = new addrMac();
                clntSes = -1;
                break;
            case packPppOE.codePadO:
            case packPppOE.codePadS:
                break;
            default:
                return;
        }
    }

    /**
     * send one packet
     *
     * @param pck packet to send
     */
    protected void doTxPack(packHolder pck) {
        if (clntSes == -1) {
            cntr.drop(pck, counter.reasons.notInTab);
            return;
        }
        cntr.rx(pck);
        pck.getSkip(2);
        pck.ETHtrg.setAddr(clntAdr);
        pck.ETHsrc.setAddr(hwaddr);
        packPppOE.updateHeader(pck, packPppOE.codeData, clntSes);
        lower.sendPack(pck);
    }

    /**
     * get session info
     *
     * @param mac mac address
     * @return session id, negative if down
     */
    public int getSession(addrMac mac) {
        mac.setAddr(clntAdr);
        return clntSes;
    }

}

class ifcP2pOErelySer implements ifcUp {

    public final ifcP2pOErely peer;

    public counter cntr = new counter();

    public ifcDn lower = new ifcNull();

    public ifcP2pOErelySer(ifcP2pOErely lower) {
        peer = lower;
    }

    public counter getCounter() {
        return cntr;
    }

    public void setState(state.states stat) {
    }

    public void closeUp() {
    }

    public void setParent(ifcDn parent) {
        lower = parent;
    }

    public void recvPack(packHolder pck) {
        peer.doTxPack(pck);
    }

}

class ifcP2pOErelyDial implements ifcDn {

    public final ifcP2pOErely peer;

    public counter cntr = new counter();

    public ifcUp upper = new ifcNull();

    public ifcP2pOErelyDial(ifcP2pOErely lower) {
        peer = lower;
    }

    public counter getCounter() {
        return cntr;
    }

    public void sendPack(packHolder pck) {
        peer.doTxPack(pck);
    }

    public addrType getHwAddr() {
        return new addrEmpty();
    }

    public void setFilter(boolean promisc) {
    }

    public state.states getState() {
        return state.states.up;
    }

    public void closeDn() {
    }

    public void flapped() {
    }

    public int getMTUsize() {
        return peer.lower.getMTUsize() - packPppOE.size;
    }

    public long getBandwidth() {
        return peer.lower.getBandwidth();
    }

    public void setUpper(ifcUp server) {
        upper = server;
        upper.setParent(this);
    }

}
