/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.config;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.xml.transform.TransformerException;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.text.LexicalCast;
import net.ranides.assira.text.StringUtils;
import net.ranides.assira.xml.NodeCollection;
import net.ranides.assira.xml.NodeType;
import net.ranides.assira.xml.XMLTransform;
import org.w3c.dom.Node;

/**
 * Klasa obsługująca dostęp do plików z konfiguracją w formacie JSON.
 * @author ranides
 */
public class XMLConfiguration extends AbstractConfiguration {

    private static final FPath ROOT_FPATH = new FPath();

    /**
     * Tworzy nową konfigurację powiązaną z zasobem wskazywanym przez ścieżkę.
     * Wskazany zasób musi prowadzić do treści w formacie JSON.
     * @param path
     */
    public XMLConfiguration(String path) {
        super(path);
    }

    @Override
    public Configuration load() throws ConfigException {
        try {
            if("file".equals(protocol)) {
                values = new LinkedHashMap<>();
                putNode(values, XMLTransform.parse(new File(location)), ROOT_FPATH, 0);
                return this;
            }
            if("url".equals(protocol)) {
                values = new LinkedHashMap<>();
                putNode(values, XMLTransform.parse(new URL(location)), ROOT_FPATH, 0);
                return this;
            }
        } catch(IOException cause) {
            throw new ConfigException(cause);
        } catch(TransformerException cause) {
            throw new ConfigException(cause);
        }
        return this;
    }

    @Override
    public Configuration save() throws ConfigException {
        try {
            if("file".equals(protocol)) {
                // XMLTransform.source(new File(location), null);
                // return this;
                // @todo (prev # 8) XMLConfiguration.save
                throw new IOException("N/A");
            }
        } catch(IOException cause) {
            throw new ConfigException(cause);
        }
        throw new ConfigException("You can save configuration to files only.");
    }

    static void putNode(Map<String, Object> target, Node node, FPath path, int index) {
        NodeType type = NodeType.valueOf(node);

        FPath prefix;
        if(type == NodeType.DOCUMENT) {
            prefix = path;
        } else {
            prefix = new FPath(path, node.getNodeName(), index);
        }

        int count = 0;
        String prev = null;

        for(Node item : NodeCollection.nodes(node)) {
            if(ValueUtils.equals(item.getNodeName(), prev)) {
                count++;
            }
            if(NodeType.ELEMENT.match(item)) {
                prev = item.getNodeName();
                putNode(target, item, prefix, count);
                continue;
            }
            if(NodeType.TEXT.match(item)) {
                String text = StringUtils.trim(item.getTextContent());
                if(!text.isEmpty()) {
                    putValue(target, prefix, text);
                }
                continue;
            }
        }

    }

    @SuppressWarnings("unchecked")
    static void putValue(Map<String, Object> target, FPath path, String value) {
        Map<String, Object> now = target;
        for(FItem item : path) {
            // wstawiamy pierwszy element, jeśli coś już siedzi, to jest to map
            if( item.index==0 ) {
                Object prev = now.get(item.name);
                if(prev instanceof Map) {
                    now = (Map)prev;
                } else {
                    now = indent(now, item.name);
                }
            }
            // wstawiamy drugi element, transformacja na listę...
            else if( item.index==1 ) {
                Object prev = now.get(item.name);
                if(prev instanceof List) {
                    now = (Map)(((List)prev).get(item.index));
                } else {
                    now = pair(now, item.name);
                }
            // wstawiamy kolejne elementy, lista musi już siedzieć
            } else {
                List<Object> prev = (List<Object>)now.get(item.name);
                if( item.index < prev.size() ) {
                    now = (Map)prev.get(item.index);
                } else {
                    now = indent(prev);
                }
            }
        }

        FItem head = path.head();
        // wstawiamy pierwszy element, nic nie powinno siedzieć...
        if(head.index == 0) {
            now.put(head.name, value);
        }
        // wstawiamy drugi element, "coś" powinno siedzieć
        else if(head.index == 1) {
            pair(now, head.name, value);
        }
        // wstawiamy kolejne elementy, lista musi już siedzieć
        else {
            ((List<Object>)now.get(head.name)).add(value);
        }
    }

    private static Map<String, Object> indent(Map<String, Object> target, String name) {
        Map<String, Object> map = new LinkedHashMap<>();
        target.put(name, map);
        return map;
    }

    private static Map<String, Object> indent(List<Object> target) {
        Map<String, Object> map = new LinkedHashMap<>();
        target.add(map);
        return map;
    }

    private static Map<String, Object> pair(Map<String, Object> target, String name) {
        return pair(target, name, new LinkedHashMap<String, Object>());
    }

    @SuppressWarnings("PMD.UnusedPrivateMethod") // false positive
    private static <T> T pair(Map<String, Object> target, String name, T value) {
        List<Object> nlist = new LinkedList<>();
        nlist.add( target.get(name));
        nlist.add( value );
        target.put(name, nlist);
        return value;
    }

    static final class FItem {
        final String name;
        final int index;

        public FItem(String name, int index) {
            this.name = name;
            this.index = index;
        }

        @Override
        public String toString() {
            return name + '[' + index + ']';
        }

    }

    static final class FPath implements Iterable<FItem> {

        FItem[] items;

        public FPath() {
            this.items = new FItem[0];
        }

        public FPath(String name, int index) {
            this(ROOT_FPATH, name, index);
        }

        public FPath(FPath root, String name, int index) {
            this(root, name.split("\\."), index);
        }

        FPath(FPath root, String[] names, int index) {
            int n = names.length;
            int offset = root.items.length;
            this.items = Arrays.copyOf(root.items, offset+n);
            for(int i=0; i<n; i++) {
                this.items[offset+i] = new FItem(names[i], index);
            }
        }

        @Override
        public String toString() {
            return StringUtils.join(LexicalCast.asList(items), " / ");
        }

        @Override
        public Iterator<FItem> iterator() {
            return ArrayUtils.iterator(items, items.length-1, 0);
        }

        public FItem head() {
            return items.length>0 ? items[items.length-1] : null;
        }

    }
}
