package org.jgroups.protocols.relay.config;

import org.jgroups.JChannel;
import org.jgroups.stack.Protocol;
import org.w3c.dom.*;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.*;

/**
 * Parses and maintains the RELAY2 configuration (in memory)
 * @author Bela Ban
 * @since 3.2
 */
public class RelayConfig {
    protected static final String RELAY_CONFIG  = "RelayConfiguration";
    protected static final String SITES         = "sites";
    protected static final String SITE          = "site";
    protected static final String BRIDGES       = "bridges";
    protected static final String BRIDGE        = "bridge";
    protected static final String FORWARDS      = "forwards";
    protected static final String FORWARD       = "forward";
    


    /*public String toString() {
        StringBuilder sb=new StringBuilder();
        sb.append("sites:\n");
        for(Map.Entry<String,SiteConfig> entry: sites.entrySet())
            sb.append(entry.getKey() + " --> " + entry.getValue() + "\n");

        return sb.toString();
    }*/

    /** Parses site names and their configuration (e.g. "nyc" --> SiteConfig) into the map passed as argument */
    public static void parse(InputStream input, final Map<String,SiteConfig> map) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(false); // for now
        DocumentBuilder builder=factory.newDocumentBuilder();
        Document document=builder.parse(input);
        Element root=document.getDocumentElement();
        match(RELAY_CONFIG, root.getNodeName(), true);
        NodeList children=root.getChildNodes();
        if(children == null || children.getLength() == 0)
            return;
        for(int i=0; i < children.getLength(); i++) {
            Node node=children.item(i);
            if(node.getNodeType() != Node.ELEMENT_NODE)
                continue;
            String element_name=node.getNodeName();
            if(SITES.equals(element_name))
                parseSites(map, node);
            else
                throw new Exception("expected <" + SITES + ">, but got " + "<" + element_name + ">");
        }
    }


    protected static void parseSites(final Map<String,SiteConfig> map, Node root) throws Exception {
        NodeList children=root.getChildNodes();
        if(children == null || children.getLength() == 0)
            return;
        for(int i=0; i < children.getLength(); i++) {
            Node node=children.item(i);
            if(node.getNodeType() != Node.ELEMENT_NODE)
                continue;
            match(SITE, node.getNodeName(), true);
            NamedNodeMap attrs=node.getAttributes();
            if(attrs == null || attrs.getLength() == 0)
                continue;
            Attr name_attr=(Attr)attrs.getNamedItem("name");
            Attr id_attr=(Attr)attrs.getNamedItem("id");

            String name=name_attr.getValue();
            short id=Short.parseShort(id_attr.getValue());
            if(id < 0)
                throw new Exception("Site ID must be >= 0");

            if(map.containsKey(name))
                throw new Exception("Site \"" + name + "\" already defined");
            SiteConfig site_config=new SiteConfig(name, id);
            map.put(name, site_config);

            parseBridgesAndForwards(site_config, node);
        }

        // Verify that all sites have unique IDs
        Set<Short> ids=new HashSet<Short>();
        for(SiteConfig site_config: map.values()) {
            if(ids.contains(site_config.getId()))
                throw new Exception("Site ID \"" + site_config.getId() + "\" is defined multiple times (must be unique)");
            ids.add(site_config.getId());
        }
    }

    protected static void parseBridgesAndForwards(SiteConfig site_config, Node root) throws Exception {
        NodeList children=root.getChildNodes();
        if(children == null || children.getLength() == 0)
            return;
        for(int i=0; i < children.getLength(); i++) {
            Node node=children.item(i);
            if(node.getNodeType() != Node.ELEMENT_NODE)
                continue;
            String node_name=node.getNodeName();

            if(BRIDGES.equals(node_name))
                parseBridges(site_config, node);
            else if(FORWARDS.equals(node_name))
                parseForwards(site_config, node);
            else
                throw new Exception("expected \"" + BRIDGES + "\" or \"" + FORWARDS + "\" keywords");
        }
    }

    protected static void parseBridges(SiteConfig site_config, Node root) throws Exception {
        NodeList children=root.getChildNodes();
        if(children == null || children.getLength() == 0)
            return;
        for(int i=0; i < children.getLength(); i++) {
            Node node=children.item(i);
            if(node.getNodeType() != Node.ELEMENT_NODE)
                continue;
            String node_name=node.getNodeName();
            match(BRIDGE, node_name, true);

            NamedNodeMap attrs=node.getAttributes();
            if(attrs == null || attrs.getLength() == 0)
                continue;
            Attr name_attr=(Attr)attrs.getNamedItem("name");
            Attr config_attr=(Attr)attrs.getNamedItem("config");
            String name=name_attr != null? name_attr.getValue() : null;
            String config=config_attr.getValue();
            BridgeConfig bridge_config=new PropertiesBridgeConfig(name, config);
            site_config.addBridge(bridge_config);
        }
    }

    protected static void parseForwards(SiteConfig site_config, Node root) throws Exception {
        NodeList children=root.getChildNodes();
        if(children == null || children.getLength() == 0)
            return;
        for(int i=0; i < children.getLength(); i++) {
            Node node=children.item(i);
            if(node.getNodeType() != Node.ELEMENT_NODE)
                continue;
            String node_name=node.getNodeName();
            match(FORWARD, node_name, true);

            NamedNodeMap attrs=node.getAttributes();
            if(attrs == null || attrs.getLength() == 0)
                continue;
            Attr to_attr=(Attr)attrs.getNamedItem("to");
            Attr gw_attr=(Attr)attrs.getNamedItem("gateway");
            String to=to_attr.getValue();
            String gateway=gw_attr.getValue();
            ForwardConfig forward_config=new ForwardConfig(to, gateway);
            site_config.addForward(forward_config);
        }
    }


    protected static void match(String expected_name, String name, boolean is_element) throws Exception {
        if(!expected_name.equals(name))
            throw new Exception((is_element? "Element " : "Attribute ") + "\"" + name + "\" didn't match \"" + expected_name + "\"");
    }

    public static class SiteConfig {
        protected final String              name;
        protected final short               id;
        protected final List<BridgeConfig>  bridges=new ArrayList<BridgeConfig>();
        protected final List<ForwardConfig> forwards=new ArrayList<ForwardConfig>();

        public SiteConfig(String name, short id) {
            this.name=name;
            this.id=id;
        }

        public short  getId()   {return id;}
        public String getName() {return name;}

        public List<BridgeConfig>  getBridges()   {return bridges;}
        public List<ForwardConfig> getForwards()  {return forwards;}

        public SiteConfig addBridge(BridgeConfig bridge_config)    {bridges.add(bridge_config);   return this;}
        public SiteConfig addForward(ForwardConfig forward_config) {forwards.add(forward_config); return this;}

        public String toString() {
            StringBuilder sb=new StringBuilder("name=" + name + " (id=" + id + ")\n");
            if(!bridges.isEmpty())
                for(BridgeConfig bridge_config: bridges)
                    sb.append(bridge_config).append("\n");
            if(!forwards.isEmpty())
                for(ForwardConfig forward_config: forwards)
                    sb.append(forward_config).append("\n");
            return sb.toString();
        }
    }

    public abstract static class BridgeConfig {
        protected final String cluster_name;

        protected BridgeConfig(String cluster_name) {this.cluster_name=cluster_name;}

        public String            getClusterName()  {return cluster_name;}
        public abstract JChannel  createChannel() throws Exception;

        public String toString() {return "cluster=" + cluster_name;}
    }

    public static class PropertiesBridgeConfig extends BridgeConfig {
        protected final String config;

        public PropertiesBridgeConfig(String cluster_name, String config) {
            super(cluster_name);
            this.config=config;
        }

        public JChannel createChannel() throws Exception {return new JChannel(config);}
        public String toString() {return "config=" + config + super.toString();}
    }


    public static class ProgrammaticBridgeConfig extends BridgeConfig {
        protected Protocol[] protocols;

        public ProgrammaticBridgeConfig(String cluster_name, Protocol[] prots) {
            super(cluster_name);
            this.protocols=prots;
        }

        public JChannel createChannel() throws Exception {
            return new JChannel(protocols);
        }

        public String toString() {
            return super.toString() + ", protocols=" + printProtocols(protocols);
        }


        protected static String printProtocols(Protocol[] protocols) {
            StringBuilder sb=new StringBuilder("[");
            boolean first=true;
            for(Protocol prot: protocols) {
                if(first)
                    first=false;
                else
                    sb.append(", ");
                sb.append(prot.getName());
            }
            sb.append("]");
            return sb.toString();
        }
    }


    public static class ForwardConfig {
        protected final String to;
        protected final String gateway;

        public ForwardConfig(String to, String gateway) {
            this.to=to;
            this.gateway=gateway;
        }

        public String getGateway() {return gateway;}
        public String getTo()      {return to;}

        public String toString() {
            return "forward to=" + to + " gateway=" + gateway;
        }
    }

    public static void main(String[] args) throws Exception {
        InputStream input=new FileInputStream("/home/bela/relay2.xml");
        Map<String,SiteConfig> sites=new HashMap<String,SiteConfig>();
        RelayConfig.parse(input, sites);
        System.out.println("sites:");
        for(Map.Entry<String,SiteConfig> entry: sites.entrySet())
            System.out.println(entry.getKey() + ":\n" + entry.getValue() + "\n");

    }
}
