package org.bdware.sc;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bdware.sc.db.CMTables;
import org.bdware.sc.db.KeyValueDBUtil;
import org.bdware.sc.util.JsonUtil;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

public class ContractPort {
    private static final Logger LOGGER = LogManager.getLogger(ContractPort.class);
    private static final String USED_PORTS = "USED_PORTS";

    private final AtomicInteger port;
    private final int beginningPort;
    private final Set<Integer> usedPorts;
    private int CMPort;

    public ContractPort(int startPort) {
        port = new AtomicInteger(startPort);
        beginningPort = startPort;

        usedPorts = new HashSet<>();
    }

    public int getPortAndInc() {
        int portInt = port.getAndIncrement();
        if (portInt > 65535) {
            port.set(beginningPort);
            portInt = beginningPort;
        }
        return portInt;
    }

    public void reSetPort(int p) {
        if (p > 65535) {
            port.set(beginningPort);
        } else {
            port.set(p);
        }
    }

    public int getCMPort() {
        return CMPort;
    }

    public void setCMPort(int cmPort) {
        CMPort = cmPort;
        LOGGER.info("cmPort is set to " + cmPort);
    }

    public void visitReconnectPortRange(PortVisitor v) {
        try {
            int[] usedPorts = JsonUtil.fromJson(
                    KeyValueDBUtil.instance.getValue(CMTables.ContractInfo.toString(), USED_PORTS),
                    int[].class);
            if (null == usedPorts || usedPorts.length == 0) {
                throw new NullPointerException();
            }
            LOGGER.info("visit CP port in " + Arrays.toString(usedPorts));
            int maxUsedPort = -1;
            for (int port : usedPorts) {
                if (v.visit(port)) {
                    updateUsedPorts(port, true);
                    if (port > maxUsedPort) {
                        maxUsedPort = port;
                    }
                }
            }
            if (maxUsedPort > this.port.get()) {
                this.port.set(maxUsedPort + 1);
            }
        } catch (Exception ignored) {
            int endPort = beginningPort + 30;
            for (int port = beginningPort; port < endPort; ++port) {
                if (v.visit(port)) {
                    updateUsedPorts(port, true);
                }
            }
            LOGGER.info("visit CP port form 1616 to 1646");
            for (int port = 1616; port < 1646; ++port) {
                if (port >= beginningPort && port < endPort) {
                    continue;
                }
                if (v.visit(port)) {
                    updateUsedPorts(port, true);
                }
            }
        }
        updateDb();
    }

    public void updateDb(int port, boolean isAdd) {
        updateUsedPorts(port, isAdd);
        updateDb();
    }

    private void updateUsedPorts(int port, boolean isAdd) {
        if (isAdd) {
            usedPorts.add(port);
        } else {
            usedPorts.remove(port);
        }
    }

    private synchronized void updateDb() {
        KeyValueDBUtil.instance.setValue(CMTables.ContractInfo.toString(), USED_PORTS, JsonUtil.toJson(usedPorts));
    }

    public interface PortVisitor {
        /**
         * check if the port is listened
         *
         * @param port port number
         * @return whether some program listens to the port
         */
        boolean visit(int port);
    }
}
