package org.freertr.ifc;

import java.util.List;
import org.freertr.addr.addrEmpty;
import org.freertr.addr.addrType;
import org.freertr.pack.packHolder;
import org.freertr.user.userHelp;
import org.freertr.util.bits;
import org.freertr.util.cmds;
import org.freertr.util.counter;
import org.freertr.util.debugger;
import org.freertr.util.logger;
import org.freertr.util.state;

/**
 * frame relay encapsulation handler
 *
 * @author matecsaba
 */
public class ifcFrameRelay implements ifcUp, ifcDn {

    /**
     * ethertype
     */
    public final static int ethtyp = 0x6559;

    /**
     * terminal mode
     */
    public int dataMode = dataMoDTE;

    /**
     * lmi type
     */
    public int lmiType = lmiAnsi;

    /**
     * dlci number
     */
    public int dlciNum = 1;

    /**
     * max payload size
     */
    public int fragLen = 0;

    /**
     * max payload delay
     */
    public int fragGap = 0;

    /**
     * tx sequence
     */
    public int fragSeqTx = 0;

    /**
     * rx sequence
     */
    public int fragSeqRx = -1;

    /**
     * reassembly buffer
     */
    public packHolder fragReasm = new packHolder(true, true);

    /**
     * ansi lmi type
     */
    public final static int lmiAnsi = 1;

    /**
     * cisco lmi type
     */
    public final static int lmiCisco = 2;

    /**
     * q933a lmi type
     */
    public final static int lmiQ933a = 3;

    /**
     * dte terminal
     */
    public final static int dataMoDTE = 1;

    /**
     * dce terminal
     */
    public final static int dataMoDCE = 2;

    /**
     * nni terminal
     */
    public final static int dataMoNNI = 3;

    /**
     * size of header
     */
    public final static int size = 2;

    /**
     * type of fragment header
     */
    public final static int fragType = 0x3b1;

    /**
     * first fragment
     */
    public final static int fragBeg = 0x8000;

    /**
     * last fragment
     */
    public final static int fragEnd = 0x4000;

    /**
     * current keepalive interval (0=disabled)
     */
    public int keepaliveInterval = 5000;

    /**
     * last known state
     */
    public state.states lastState = state.states.down;

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

    /**
     * server that handler received packets
     */
    public ifcUp upper = new ifcNull();

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

    /**
     * transmit sequence number
     */
    public int sequenceTx = 0;

    /**
     * receive sequence number
     */
    public int sequenceRx = 0;

    /**
     * time when last keepalive received
     */
    public long lastRxKeep = 0;

    /**
     * time when last keepalive sent
     */
    public long lastTxKeep = 0;

    /**
     * need full update
     */
    public boolean needFull;

    /**
     * enquery
     */
    public final static int opcodeEnquery = 0x75;

    /**
     * status
     */
    public final static int opcodeStatus = 0x7d;

    /**
     * keepalive
     */
    protected ifcFrameRelayTxKeep keepTimer;

    public counter getCounter() {
        return cntr;
    }

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

    /**
     * close interface
     */
    public void closeUp() {
        setState(state.states.close);
        upper.closeUp();
        restartTimer(true);
    }

    public void closeDn() {
        setState(state.states.close);
        lower.closeDn();
        restartTimer(true);
    }

    public void flapped() {
        lower.flapped();
    }

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

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

    public void setFilter(boolean promisc) {
    }

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

    /**
     * set state
     *
     * @param stat state
     */
    public void setState(state.states stat) {
        stat = state.toForceable(stat);
        if (checkPeerState(stat)) {
            return;
        }
        restartTimer(false);
    }

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

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

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

    /**
     * create new instance
     */
    public ifcFrameRelay() {
        restartTimer(false);
    }

    /**
     * get help text
     *
     * @param l storage
     */
    public static void getHelp(userHelp l) {
        l.add(null, false, 2, new int[]{3}, "keepalive", "keepalive timer");
        l.add(null, false, 3, new int[]{-1}, "<num>", "time in ms");
        l.add(null, false, 2, new int[]{3}, "mode", "my line mode");
        l.add(null, false, 3, new int[]{-1}, "dce", "this side is dce");
        l.add(null, false, 3, new int[]{-1}, "dte", "this side is dte");
        l.add(null, false, 3, new int[]{-1}, "nni", "both sides are nni");
        l.add(null, false, 2, new int[]{3}, "lmi", "lmi type");
        l.add(null, false, 3, new int[]{-1}, "ansi", "set to ansi");
        l.add(null, false, 3, new int[]{-1}, "cisco", "set to cisco");
        l.add(null, false, 3, new int[]{-1}, "q933a", "set to itu q933a");
        l.add(null, false, 2, new int[]{3}, "dlci", "set dlci number");
        l.add(null, false, 3, new int[]{-1}, "<num>", "dlci number");
        l.add(null, false, 2, new int[]{3}, "fragment", "set end2end payload size");
        l.add(null, false, 3, new int[]{-1}, "<num>", "number of bytes");
        l.add(null, false, 2, new int[]{3}, "frgap", "inter fragment gap");
        l.add(null, false, 3, new int[]{-1}, "<num>", "milliseconds");
    }

    /**
     * get configuration
     *
     * @param l storage
     * @param beg beginning
     */
    public void getConfig(List<String> l, String beg) {
        l.add(beg + "keepalive " + keepaliveInterval);
        String a;
        switch (dataMode) {
            case dataMoDCE:
                a = "dce";
                break;
            case dataMoDTE:
                a = "dte";
                break;
            case dataMoNNI:
                a = "nni";
                break;
            default:
                a = "unknown=" + dataMode;
                break;
        }
        l.add(beg + "mode " + a);
        switch (lmiType) {
            case lmiAnsi:
                a = "ansi";
                break;
            case lmiCisco:
                a = "cisco";
                break;
            case lmiQ933a:
                a = "q933a";
                break;
            default:
                a = "unknown=" + lmiType;
                break;
        }
        l.add(beg + "lmi " + a);
        l.add(beg + "dlci " + dlciNum);
        l.add(beg + "fragment " + fragLen);
        l.add(beg + "frgap " + fragGap);
    }

    /**
     * do configuration
     *
     * @param cmd command
     */
    public void doConfig(cmds cmd) {
        String a = cmd.word();
        if (a.equals("keepalive")) {
            keepaliveInterval = bits.str2num(cmd.word());
            restartTimer(false);
            return;
        }
        if (a.equals("mode")) {
            a = cmd.word();
            int i = 0;
            if (a.equals("dce")) {
                i = dataMoDCE;
            }
            if (a.equals("dte")) {
                i = dataMoDTE;
            }
            if (a.equals("nni")) {
                i = dataMoNNI;
            }
            dataMode = i;
            return;
        }
        if (a.equals("lmi")) {
            a = cmd.word();
            int i = 0;
            if (a.equals("ansi")) {
                i = lmiAnsi;
            }
            if (a.equals("cisco")) {
                i = lmiCisco;
            }
            if (a.equals("q933a")) {
                i = lmiQ933a;
            }
            lmiType = i;
            return;
        }
        if (a.equals("dlci")) {
            dlciNum = bits.str2num(cmd.word());
            return;
        }
        if (a.equals("fragment")) {
            fragLen = bits.str2num(cmd.word());
            return;
        }
        if (a.equals("frgap")) {
            fragGap = bits.str2num(cmd.word());
            return;
        }
        cmd.badCmd();
    }

    /**
     * undo configuration
     *
     * @param cmd command
     */
    public void unConfig(cmds cmd) {
        String a = cmd.word();
        if (a.equals("fragment")) {
            fragLen = 0;
            return;
        }
        if (a.equals("frgap")) {
            fragGap = 0;
            return;
        }
        cmd.badCmd();
    }

    /**
     * check peer state from timers
     *
     * @param force try to force line protocol
     * @return true if state changed
     */
    public boolean checkPeerState(state.states force) {
        state.states stat = state.states.down;
        if (bits.getTime() - lastRxKeep < keepaliveInterval * 6) {
            stat = state.states.up;
        }
        if (keepaliveInterval < 1) {
            stat = state.states.up;
        }
        if (state.toForceable(force) == state.states.admin) {
            stat = state.states.admin;
        }
        if (lastState == stat) {
            return false;
        }
        if (stat != state.states.up) {
            lastRxKeep = 0;
            sequenceRx = 0;
        }
        lastState = stat;
        if (debugger.ifcFrmRlyEvnt) {
            logger.debug("line proto=" + state.conv2string(stat));
        }
        if (stat != state.states.up) {
            lower.flapped();
        }
        cntr.stateChange(stat);
        upper.setState(stat);
        return true;
    }

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

    private void putDLCI(packHolder pck, int dlci) {
        pck.putByte(0, (dlci >>> 4) << 2);
        pck.putByte(1, (dlci << 4) | 1);
        pck.putSkip(size);
    }

    private void putFrag(packHolder pck, int seq, boolean frst, boolean last) {
        pck.msbPutW(0, fragType);
        pck.putSkip(size);
        int i = (seq & 0xf00) << 1;
        if (frst) {
            i |= fragBeg;
        }
        if (last) {
            i |= fragEnd;
        }
        i |= seq & 0xff;
        pck.msbPutW(0, i);
        pck.putSkip(size);
    }

    /**
     * received packet
     *
     * @param pck packet
     */
    public void recvPack(packHolder pck) {
        cntr.rx(pck);
        if (pck.dataSize() < size) {
            cntr.drop(pck, counter.reasons.tooSmall);
            return;
        }
        int b0 = pck.getByte(0);
        int b1 = pck.getByte(1);
        pck.getSkip(size);
        // boolean cr = (b0 & 0x2) != 0;
        // boolean de = (b1 & 0x2) != 0;
        // boolean becn = (b1 & 0x4) != 0;
        // boolean fecn = (b1 & 0x8) != 0;
        int dlci = ((b0 >>> 2) << 4) | (b1 >>> 4);
        if (dlci != dlciNum) {
            if (dlci != getLMIdlci()) {
                cntr.drop(pck, counter.reasons.badVlan);
                return;
            }
            recvKeepalive(pck);
            checkPeerState(state.states.up);
            return;
        }
        if (pck.msbGetW(0) != fragType) {
            upper.recvPack(pck);
            return;
        }
        pck.getSkip(size);
        int i = pck.msbGetW(0);
        pck.getSkip(size);
        int seq = ((i >>> 1) & 0xf00) | (i & 0xff);
        if ((i & fragBeg) != 0) {
            fragSeqRx = seq;
            fragReasm.clear();
        }
        if (fragSeqRx != seq) {
            cntr.drop(pck, counter.reasons.badRxSeq);
            return;
        }
        fragSeqRx = (fragSeqRx + 1) & 0xfff;
        byte[] buf = pck.getCopy();
        fragReasm.putCopy(buf, 0, 0, buf.length);
        fragReasm.putSkip(buf.length);
        fragReasm.merge2end();
        if ((i & fragEnd) == 0) {
            return;
        }
        fragSeqRx = -1;
        upper.recvPack(fragReasm);
    }

    public void sendPack(packHolder pck) {
        cntr.tx(pck);
        if ((fragLen < 1) || (pck.dataSize() < fragLen)) {
            putDLCI(pck, dlciNum);
            pck.merge2beg();
            lower.sendPack(pck);
            return;
        }
        byte[] buf = pck.getCopy();
        for (int ofs = 0; ofs < buf.length;) {
            int len = buf.length - ofs;
            if (len > fragLen) {
                len = fragLen;
            }
            fragSeqTx++;
            pck.clear();
            putDLCI(pck, dlciNum);
            putFrag(pck, fragSeqTx, ofs == 0, (ofs + len) >= buf.length);
            pck.msbPutW(ofs, len);
            pck.putCopy(buf, ofs, 0, len);
            pck.putSkip(len);
            pck.merge2beg();
            lower.sendPack(pck);
            ofs += len;
            if (fragGap < 1) {
                continue;
            }
            bits.sleep(fragGap);
        }
    }

    private int getLMIdlci() {
        switch (lmiType) {
            case lmiAnsi:
                return 0;
            case lmiCisco:
                return 1023;
            case lmiQ933a:
                return 0;
            default:
                return -1;
        }
    }

    private int getNLPIDvalue() {
        switch (lmiType) {
            case lmiAnsi:
                return 0x0308;
            case lmiCisco:
                return 0x0309;
            case lmiQ933a:
                return 0x0308;
            default:
                return -1;
        }
    }

    private int getORer(int i) {
        if (lmiType == lmiQ933a) {
            return i | 0x50;
        } else {
            return i;
        }
    }

    private void putPVCstat(packHolder pck, int dlci) {
        pck.putByte(0, getORer(7)); // type of pvcstat
        switch (lmiType) {
            case lmiAnsi:
            case lmiQ933a:
                pck.putByte(1, 3); // length of pvcstat
                pck.putByte(2, (dlci >>> 4) & 0x3f);
                pck.putByte(3, (dlci << 3) | 0x80);
                pck.putByte(4, 0x82);
                pck.putSkip(5);
                break;
            case lmiCisco:
                pck.putByte(1, 6); // length of pvcstat
                pck.msbPutW(2, dlci); // dlci number
                pck.putByte(4, 2);
                pck.putByte(5, 0);
                pck.putByte(6, 0);
                pck.putByte(7, 0);
                pck.putSkip(8);
                break;
        }
    }

    /**
     * send keepalive packet
     */
    protected void sendKeepalive() {
        lastTxKeep = bits.getTime();
        sequenceTx++;
        if (debugger.ifcFrmRlyEvnt) {
            logger.debug("tx keepalive mySeq=" + sequenceTx + " peerSeq=" + sequenceRx + " full=" + needFull);
        }
        packHolder pck = new packHolder(true, true);
        int i;
        pck.msbPutW(0, getNLPIDvalue()); // nlpid value
        pck.putByte(2, 0); // call reference
        switch (dataMode) {
            case dataMoDTE:
                i = opcodeEnquery;
                break;
            case dataMoDCE:
                i = opcodeStatus;
                break;
            case dataMoNNI:
                if ((sequenceTx & 1) == 0) {
                    i = opcodeEnquery;
                } else {
                    i = opcodeStatus;
                }
                break;
            default:
                return;
        }
        pck.putByte(3, i); // opcode
        pck.putSkip(4);
        if (lmiType == lmiAnsi) {
            pck.putByte(0, 0x95); // ansi lockshift
            pck.putSkip(1);
        }
        pck.putByte(0, getORer(1)); // type of report
        pck.putByte(1, 1); // length of report
        if (needFull) {
            i = 0;
        } else {
            i = 1;
        }
        pck.putByte(2, i); // type of listing
        pck.putSkip(3);
        pck.putByte(0, getORer(3)); // type of alive
        pck.putByte(1, 2); // length of alive
        pck.putByte(2, sequenceTx);
        pck.putByte(3, sequenceRx);
        pck.putSkip(4);
        if (needFull) {
            putPVCstat(pck, dlciNum);
        }
        pck.merge2beg();
        putDLCI(pck, getLMIdlci());
        pck.merge2beg();
        lower.sendPack(pck);
        needFull = false;
    }

    /**
     * received keepalive packet
     *
     * @param pck packet
     */
    protected void recvKeepalive(packHolder pck) {
        lastRxKeep = bits.getTime();
        needFull = false;
        int i = pck.msbGetW(0); // nlpid
        if (i != getNLPIDvalue()) {
            cntr.drop(pck, counter.reasons.badHdr);
            return;
        }
        i = pck.getByte(2); // call reference
        if (i != 0) {
            return;
        }
        i = pck.getByte(3); // opcode
        pck.getSkip(4);
        switch (i) {
            case opcodeEnquery:
                break;
            case opcodeStatus:
                break;
            default:
                return;
        }
        if (lmiType == lmiAnsi) {
            i = pck.getByte(0); // ansi lockshift
            pck.getSkip(1);
            if (i != 0x95) {
                return;
            }
        }
        i = pck.getByte(0); // report type
        if (i != getORer(1)) {
            return;
        }
        i = pck.getByte(1); // report size
        if (i != 1) {
            return;
        }
        i = pck.getByte(2); // report value
        pck.getSkip(3);
        needFull = (i == 0);
        i = pck.getByte(0); // alive
        pck.getSkip(1);
        if (i != getORer(3)) {
            return;
        }
        i = pck.getByte(0); // size of sequences
        int seqPeer = pck.getByte(1); // peer sequence
        int seqMine = pck.getByte(2); // peer sequence
        pck.getSkip(3);
        if (i != 2) {
            return;
        }
        sequenceRx = seqPeer;
        if (debugger.ifcFrmRlyEvnt) {
            logger.debug("rx keepalive mySeq=" + seqMine + " peerSeq=" + seqPeer + " full=" + needFull);
        }
    }

}

class ifcFrameRelayTxKeep implements Runnable {

    private ifcFrameRelay lower;

    public ifcFrameRelayTxKeep(ifcFrameRelay parent) {
        lower = parent;
    }

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

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

}
