package com.moex.asts.export;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class Description {

    public final static int DEFAULT_CYCLES = 10000;
    
    /**
     * Объект (поле или таблица), которые могут отсутствовать в ТС.
     */
    public final static int OPTION_OPTIONAL = 0x01;
    
    public static class ParamInfo {

        /**
         * ASTS parameter name.
         */
        public final String source;
        
        /**
         * ASTS parameter value.
         */
        public final String value;
        
        ParamInfo(String source, String value) {
            this.source = source;
            this.value = value;
        }
        
    }
    
    static boolean hasOptional(int option) {
        return (option & OPTION_OPTIONAL) != 0;
    }
    
    public static class FieldInfo {
        
        /**
         * ASTS field name.
         */
        public final String source;

        public final String rename;
        
        private final int option;
        
        FieldInfo(String source, String rename, int option) {
            this.source = source;
            this.rename = rename;
            this.option = option;
        }
        
        public String alias() {
            return rename == null || rename.isEmpty() ? source : rename;
        }
        
        public boolean isOptional() {
            return hasOptional(option);
        }
        
    }
    
    final static ParamInfo[] NO_PARAMS = new ParamInfo[0]; 
    
    final static FieldInfo[] NO_FIELDS = new FieldInfo[0]; 

    public static class TableInfo {
        
        /**
         * ASTS request name.
         */
        public final String source;
        
        /**
         * ASTS master request name.
         */
        public final String master;

        public final String rename;
        
        /**
         * Maximum number of cycles to refresh
         * an updatable table until it is 
         * supposed fully loaded.
         */
        public final int cycles;

        public final ParamInfo[] params;
        
        public final FieldInfo[] fields;
        
        public final int option;
        
        TableInfo(String source, String rename, String master, int cycles, ParamInfo[] params, FieldInfo[] fields, int option) {
            this.source = source;
            this.rename = rename;
            this.master = master;
            this.cycles = cycles;
            this.params = params;
            this.fields = fields;
            this.option = option;
        }
    
        public String alias() {
            return rename == null || rename.isEmpty() ? source : rename;
        }

        public boolean isDetail() {
            return master != null && master.length() > 0;
        }
        
        public boolean isOptional() {
            return hasOptional(option);
        }
        
        static String getAttributeValue(NamedNodeMap attributes, String name, String fallback) {
            Node item = attributes.getNamedItem(name);
            if (item == null)
                return fallback;
            else
                return item.getTextContent();
        }
        
        public static TableInfo parse(Element node) throws SAXException {
            if (!node.hasAttribute("source"))
                throw new SAXException(node.getNodeName() +  
                        " missing required attribute source");
            
            String source = node.getAttribute("source");
            String master = node.getAttribute("master");
            int cycles = node.hasAttribute("cycles")
                    ? Integer.parseInt(node.getAttribute("cycles"))
                    : DEFAULT_CYCLES;
                  
            ParamInfo[] params = NO_PARAMS;
            Element nested = XmlUtil.getFirstChild(node, "params");
            if (nested != null) {
                NodeList list = nested.getElementsByTagName("param");
                if (list.getLength() > 0) {
                    Map<String, String> hash = new LinkedHashMap<>();
                    XmlUtil.loadStringMap(list, hash, "name");
                    if (hash.size() > 0) {
                        params = new ParamInfo[hash.size()];
                        int index = 0;
                        for (Entry<String, String> e : hash.entrySet())
                            params[index++] = new ParamInfo(e.getKey(), e.getValue());
                    }
                }
            }

            FieldInfo[] fields = NO_FIELDS;
            nested = XmlUtil.getFirstChild(node, "fields");
            if (nested != null) {
                NodeList list = nested.getElementsByTagName("field");
                if (list.getLength() > 0) {
                    List<FieldInfo> info = new ArrayList<>();
                    for (int i = 0; i < list.getLength(); i++) {
                        NamedNodeMap attributes = list.item(i).getAttributes();
                        Node name = attributes.getNamedItem("source");
                        if (name != null) {
                            int option = 0;
                            if ("true".equals(getAttributeValue(attributes, "optional", "")))
                                    option |= OPTION_OPTIONAL;
                            info.add(new FieldInfo(name.getTextContent(), 
                                    getAttributeValue(attributes, "rename", ""), option));
                        }
                    }
                    if (info.size() > 0) {
                        fields = new FieldInfo[info.size()];
                        info.toArray(fields);
                    }
                }
            }
            
            int option = 0;
            if (node.hasAttribute("optional") && "true".equals(node.getAttribute("optional")))
                option |= OPTION_OPTIONAL;
            return new TableInfo(source, node.getAttribute("rename"), master, cycles, params, fields, option);
        }
        
    }
    
    public static Map<String, TableInfo> parse(Element node) throws SAXException {
        Map<String, TableInfo> hash = new LinkedHashMap<>();
        NodeList list = node.getElementsByTagName("table");
        for (int i = 0; i < list.getLength(); i++) {
            TableInfo item = TableInfo.parse((Element) list.item(i));
            if (item.isDetail() && !hash.containsKey(item.master))
                 throw new SAXException("Table " + item.source + " is detail of "
                         + item.master + " which is not defined");
            hash.put(item.source, item);
        }
        return hash;
    }
    
}
