package be.iminds.ilabt.jfed.gts;

import be.iminds.ilabt.jfed.gts.model.*;
import be.iminds.ilabt.jfed.gts.model.impl.FXGtsFactory;
import be.iminds.ilabt.jfed.gts.model.impl.FXGtsType;
import be.iminds.ilabt.jfed.gts_reader.GTSBaseListener;
import be.iminds.ilabt.jfed.gts_reader.GTSLexer;
import be.iminds.ilabt.jfed.gts_reader.GTSParser;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.misc.NotNull;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.Reader;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;

import static be.iminds.ilabt.jfed.gts.GtsDslConstants.*;

/**
 * User: twalcari
 * Date: 2/5/2016
 * Time: 16:40
 */
public class GtsDslReader implements Callable<GtsType> {
    private static final Logger LOG = LoggerFactory.getLogger(GtsDslReader.class);
    private final Reader reader;

    public GtsDslReader(Reader reader) {
        this.reader = reader;
    }

    @Override
    public GtsType call() throws GtsDslParseException {
        try {
            GTSLexer lexer = new GTSLexer(new ANTLRInputStream(reader));
            CommonTokenStream tokens = new CommonTokenStream(lexer);

            GTSParser parser = new GTSParser(tokens);

            List<String> errors = new ArrayList<>();

            parser.addErrorListener(new BaseErrorListener() {
                @Override
                public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
                    errors.add(String.format("Error at [%d,%d]: %s", line, charPositionInLine, msg));
                }
            });

            FXGtsFactory factory = new FXGtsFactory();

            ParseTreeWalker walker = new ParseTreeWalker();
            final GtsDslParseListener gtsDslParseListener = new GtsDslParseListener(factory);
            walker.walk(gtsDslParseListener, parser.composite());

            if (errors.isEmpty())
                return gtsDslParseListener.getResult();
            else
                throw new GtsDslParseException(errors.stream().collect(Collectors.joining("\n")));
        } catch (IOException e) {
            throw new GtsDslParseException("Unable to read input", e);
        }
    }

    private static class GtsDslParseListener extends GTSBaseListener {
        private final FXGtsFactory factory;
        private final Deque<GtsParentNode> parentStack = new LinkedList<>();
        private FXGtsType result = null;

        public GtsDslParseListener(FXGtsFactory factory) {
            this.factory = factory;
        }

        @Override
        public void enterPort(@NotNull GTSParser.PortContext ctx) {
            parentStack.addFirst(new GtsPortParent(factory.createGtsPort()));
        }

        @Override
        public void exitPort(@NotNull GTSParser.PortContext ctx) {
            GtsParentNode firstParent = parentStack.pollFirst();

            if (!(firstParent instanceof GtsPortParent))
                throw new GtsDslParseException("Expected to find a port, put got a " + firstParent.getClass().getName());

            parentStack.peek().registerPort(((GtsPortParent) firstParent).getPort());
        }

        @Override
        public void enterAtomic(@NotNull GTSParser.AtomicContext ctx) {
            switch (ctx.ATOMICTYPE().getText()) {
                case GTS_HOST:
                    parentStack.addFirst(new GtsNodeParent(factory.createGtsHost()));
                    break;
                case GTS_OFX:
                    parentStack.addFirst(new GtsNodeParent(factory.createGtsOfx()));
                    break;
                case GTS_EXTERNAL_DOMAIN:
                    parentStack.addFirst(new GtsNodeParent(factory.createGtsExternalNetwork()));
                    break;
                case GTS_LINK:
                    parentStack.addFirst(new GtsLinkParent(factory.createGtsLink()));
                    break;
                default:
                    throw new GtsDslParseException("Unexpected atomic type: " + ctx.ATOMICTYPE().getText());
            }
        }

        @Override
        public void exitAtomic(@NotNull GTSParser.AtomicContext ctx) {
            assert parentStack.peek() instanceof GtsNodeParent || parentStack.peek() instanceof GtsLinkParent
                    : "Unexpected type on stack: " + parentStack.peek().getClass().getName();

            GtsParentNode atomic = parentStack.poll();
            if (atomic instanceof GtsNodeParent) {
                GtsNode node = ((GtsNodeParent) atomic).getNode();
                parentStack.peek().registerNode(node);
            } else if (atomic instanceof GtsLinkParent) {
                GtsLink link = ((GtsLinkParent) atomic).getLink();
                parentStack.peek().registerLink(link);
            }
        }


        @Override
        public void exitProperty(@NotNull GTSParser.PropertyContext ctx) {
            String id = ctx.id().getText();
            String value = ctx.value().getText();

            if (value.length() > 1 && value.startsWith("\"") && value.endsWith("\""))
                value = value.substring(1, value.length() - 1);

            parentStack.peek().registerProperty(id, value);
        }

        @Override
        public void exitAdjacency(@NotNull GTSParser.AdjacencyContext ctx) {
            GTSParser.PortNameContext portName1ctx = ctx.portName(0);
            GTSParser.PortNameContext portName2ctx = ctx.portName(1);

            parentStack.peek().registerAdjacency(new GtsType.Adjacency(createPortId(portName1ctx), createPortId(portName2ctx)));
        }

        private GtsAdjacency.PortId createPortId(GTSParser.PortNameContext ctx) {
            return new GtsAdjacency.PortId(
                    ctx.hostId() != null ? ctx.hostId().getText() : null, ctx.portId().getText());
        }

        @Override
        public void enterComposite(@NotNull GTSParser.CompositeContext ctx) {
            if (result != null)
                throw new GtsDslParseException("No support for nested types!");

            String typeName = ctx.ID().getText();
            result = factory.createGtsType(typeName);
            result.setDefinition(ctx.COMPOSITE_DEFINITION() != null);
            parentStack.addFirst(new GtsTypeParent(result));
        }

        public FXGtsType getResult() {
            return result;
        }


        private interface GtsParentNode {
            void registerPort(GtsPort port) throws GtsDslParseException;

            void registerNode(be.iminds.ilabt.jfed.gts.model.GtsNode node) throws GtsDslParseException;

            void registerLink(GtsLink link) throws GtsDslParseException;

            void registerProperty(String name, String value) throws GtsDslParseException;

            void registerAdjacency(GtsType.Adjacency adjacency) throws GtsDslParseException;
        }

        private static class GtsTypeParent implements GtsParentNode {
            private final GtsType type;

            public GtsTypeParent(GtsType type) {
                this.type = type;
            }

            public GtsType getType() {
                return type;
            }

            @Override
            public void registerPort(GtsPort port) throws GtsDslParseException {
                type.addPort(port);
            }

            @Override
            public void registerNode(GtsNode node) throws GtsDslParseException {
                type.addNode(node);
            }

            @Override
            public void registerLink(GtsLink link) throws GtsDslParseException {
                type.addLink(link);
            }

            @Override
            public void registerProperty(String name, String value) throws GtsDslParseException {
                switch (name) {
                    case GTS_PROP_DESCRIPTION:
                        type.setDescription(value);
                        break;

                    default:
                        type.getExtraProperties().put(name, value);
                }
            }

            @Override
            public void registerAdjacency(GtsType.Adjacency adjacency) throws GtsDslParseException {
                type.getAdjacencies().add(adjacency);
            }
        }

        private static class GtsPortParent implements GtsParentNode {
            private final GtsPort port;

            private GtsPortParent(GtsPort port) {
                this.port = port;
            }

            public GtsPort getPort() {
                return port;
            }

            @Override
            public void registerPort(GtsPort port) throws GtsDslParseException {
                throw new GtsDslParseException("Port cannot have a child port!");
            }

            @Override
            public void registerNode(GtsNode node) throws GtsDslParseException {
                throw new GtsDslParseException("Port cannot have a child node type");
            }

            @Override
            public void registerLink(GtsLink link) throws GtsDslParseException {
                throw new GtsDslParseException("Port cannot have a child link type");
            }

            @Override
            public void registerProperty(String name, String value) throws GtsDslParseException {
                switch (name) {
                    case GTS_PROP_ID:
                        port.setId(value);
                        break;
                    case GTS_PROP_MODE:
                        port.setMode(value);
                        break;
                    case GTS_PROP_DIRECTIONALITY:
                        port.setDirectionality(value);
                        break;
                    case GTS_PROP_LINERATE:
                        try {
                            port.setLineRate(Integer.parseInt(value));
                        } catch (NumberFormatException ex) {
                            LOG.warn("Could not parse lineRate to integer", ex);
                            throw new GtsDslParseException("Could not parse lineRate to integer", ex);
                        }
                        break;
                    case GTS_PROP_EDITOR_X:
                        port.setEditorX(Double.parseDouble(value));
                        break;
                    case GTS_PROP_EDITOR_Y:
                        port.setEditorY(Double.parseDouble(value));
                        break;
                    default:
                        LOG.trace("Unexpected property in port: {}={}", name, value);
                        port.getExtraProperties().put(name, value);
                        break;
                }
            }

            @Override
            public void registerAdjacency(GtsType.Adjacency adjacency) throws GtsDslParseException {
                throw new GtsDslParseException("Port cannot have an adjacency!");
            }
        }

        private static class GtsNodeParent implements GtsParentNode {

            private final GtsNode node;


            private GtsNodeParent(GtsNode node) {
                this.node = node;
            }

            @Override
            public void registerPort(GtsPort port) throws GtsDslParseException {
                node.addPort(port);
            }

            @Override
            public void registerNode(GtsNode node) throws GtsDslParseException {
                throw new GtsDslParseException("Node cannot have a child node type!");
            }

            @Override
            public void registerLink(GtsLink link) throws GtsDslParseException {
                throw new GtsDslParseException("Node cannot have a child link type!");
            }

            @Override
            public void registerProperty(String name, String value) throws GtsDslParseException {
                if (Objects.equals(GTS_PROP_ID, name))
                    node.setId(value);
                else if (Objects.equals(GTS_PROP_LOCATION, name))
                    node.setLocation(value);
                else if (Objects.equals(GTS_PROP_EDITOR_X, name))
                    node.setEditorX(Double.parseDouble(value));
                else if (Objects.equals(GTS_PROP_EDITOR_Y, name))
                    node.setEditorY(Double.parseDouble(value));

                    //Host-specific properties
                else if (Objects.equals(GTS_PROP_CPUSPEED, name) && node instanceof GtsHost)
                    ((GtsHost) node).setCpuSpeed(Double.parseDouble(value));
                else if (Objects.equals(GTS_PROP_IMAGE, name) && node instanceof GtsHost)
                    ((GtsHost) node).setImage(value);
                else if (Objects.equals(GTS_PROP_DISK, name) && node instanceof GtsHost)
                    ((GtsHost) node).setDisk(Integer.parseInt(value));

                    //OFX-specific properties
                else if (Objects.equals(GTS_PROP_CONTROLLER_IPV4, name) && node instanceof GtsOfx)
                    ((GtsOfx) node).setControllerIPv4(value);
                else if (Objects.equals(GTS_PROP_CONTROLLER_PORT, name) && node instanceof GtsOfx)
                    ((GtsOfx) node).setControllerPort(Integer.parseInt(value));
                else if (Objects.equals(GTS_PROP_FABRIC_IPV4, name) && node instanceof GtsOfx)
                    ((GtsOfx) node).setFabricIPv4(value);
                else if (Objects.equals(GTS_PROP_FABRIC_SUBNETMASK_IPV4, name) && node instanceof GtsOfx)
                    ((GtsOfx) node).setFabricSubnetmaskv4(value);

                    //external domain has no extra attributes


                    //fallback
                else
                    node.getExtraProperties().put(name, value);

            }

            @Override
            public void registerAdjacency(GtsType.Adjacency adjacency) throws GtsDslParseException {

            }

            public GtsNode getNode() {
                return node;
            }
        }

        private static class GtsLinkParent implements GtsParentNode {
            private final GtsLink link;

            private GtsLinkParent(GtsLink link) {
                this.link = link;
            }

            @Override
            public void registerPort(GtsPort port) throws GtsDslParseException {
                link.addPort(port);
            }

            @Override
            public void registerNode(GtsNode node) throws GtsDslParseException {
                throw new GtsDslParseException("Link cannot have a child atomic type");
            }

            @Override
            public void registerLink(GtsLink link) throws GtsDslParseException {
                throw new GtsDslParseException("Link cannot have a child link type");
            }

            @Override
            public void registerProperty(String name, String value) throws GtsDslParseException {
                switch (name) {
                    case GTS_PROP_ID:
                        link.setId(value);
                        break;
                    case GTS_PROP_CAPACITY:
                        link.setCapacity(Integer.parseInt(value));
                        break;
                    default:
                        link.getExtraProperties().put(name, value);

                }

            }

            @Override
            public void registerAdjacency(GtsType.Adjacency adjacency) throws GtsDslParseException {
                throw new GtsDslParseException("Link cannot have a child adjaceny");
            }

            public GtsLink getLink() {
                return link;
            }
        }


    }

    public static class GtsDslParseException extends RuntimeException {

        public GtsDslParseException() {
        }

        public GtsDslParseException(String message) {
            super(message);
        }

        public GtsDslParseException(String message, Throwable cause) {
            super(message, cause);
        }

        public GtsDslParseException(Throwable cause) {
            super(cause);
        }

        public GtsDslParseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }
}
