// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.graphview.core.access;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

import org.openstreetmap.josm.plugins.graphview.core.data.Tag;
import org.openstreetmap.josm.plugins.graphview.core.util.TagCondition;
import org.openstreetmap.josm.plugins.graphview.core.util.TagConditionLogic;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

/**
 * class to be used in SAXHandler implementations for reading implication sections of xml files
 */
public class ImplicationXMLReader {

    private final List<Implication> implications = new LinkedList<>();

    private enum State { BEFORE_IMPLICATION, BEFORE_CONDITION, CONDITION, BEFORE_IMPLIES, IMPLIES, AFTER_IMPLIES }

    private State state = State.BEFORE_IMPLICATION;

    private ConditionReader currentConditionReader;
    private TagCondition currentCondition;
    private Collection<Tag> currentImpliedTags;

    boolean tagOpen = false;

    public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {

        switch (state) {

            case BEFORE_IMPLICATION:

                if ("implication".equals(name)) {
                    state = State.BEFORE_CONDITION;
                    return;
                }
                break;

            case BEFORE_CONDITION:

                if ("condition".equals(name)) {
                    currentConditionReader = new ConditionReader();
                    state = State.CONDITION;
                    return;
                }
                break;

            case CONDITION:
                currentConditionReader.startElement(uri, localName, name, attributes);
                return;

            case BEFORE_IMPLIES:

                if ("implies".equals(name)) {
                    currentImpliedTags = new LinkedList<>();
                    state = State.IMPLIES;
                    return;
                }
                break;

            case IMPLIES:

                if ("tag".equals(name)) {
                    if (tagOpen) {
                        throw new SAXException(tr("Tag element inside other tag element"));
                    }
                    currentImpliedTags.add(readTag(attributes));
                    tagOpen = true;
                    return;
                }
                break;

            default:
                break;
        }

        //all valid paths end with return; reaching this indicates an invalid tag
        throw new SAXException(tr("Invalid opening xml tag <{0}> in state {1}", name, state));
    }

    public void endElement(String uri, String localName, String name) throws SAXException {

        switch (state) {

            case CONDITION:

                if (name.equals("condition")) {
                    if (!currentConditionReader.isFinished()) {
                        throw new SAXException(tr("Condition isn''t finished at </condition> tag"));
                    } else {
                        currentCondition = currentConditionReader.getCondition();
                        currentConditionReader = null;
                        state = State.BEFORE_IMPLIES;
                        return;
                    }
                } else {
                    currentConditionReader.endElement(uri, localName, name);
                    return;
                }

            case IMPLIES:

                if (name.equals("implies")) {
                    state = State.AFTER_IMPLIES;
                    return;
                } else if (name.equals("tag")) {
                    if (!tagOpen) {
                        throw new SAXException(tr("Closing tag element that was not open"));
                    }
                    tagOpen = false;
                    return;
                }
                break;

            case AFTER_IMPLIES:

                if (name.equals("implication")) {
                    implications.add(new Implication(currentCondition, currentImpliedTags));
                    currentCondition = null;
                    currentImpliedTags = null;
                    state = State.BEFORE_IMPLICATION;
                    return;
                }
                break;

            default:
                break;
        }

        //all valid paths end with return; reaching this indicates an invalid tag
        throw new SAXException(tr("Invalid closing xml tag </{0}> in state {1}", name, state));
    }

    public List<Implication> getImplications() throws SAXException {

        if (state != State.BEFORE_IMPLICATION) {
            throw new SAXException(tr("Some tags have not been closed; now in state {0}", state));
        } else {
            return new ArrayList<>(implications);
        }
    }

    private static Tag readTag(Attributes attributes) throws SAXException {

        String key = attributes.getValue("k");
        String value = attributes.getValue("v");

        if (key == null) {
            throw new SAXException(tr("Tag without key"));
        } else if (value == null) {
            throw new SAXException(tr("Tag without value (key is {0})", key));
        }

        return new Tag(key, value);
    }

    private static String readKey(Attributes attributes) throws SAXException {

        String key = attributes.getValue("k");

        if (key == null) {
            throw new SAXException(tr("Key element without attribute k"));
        }

        return key;
    }

    /**
     * class to be used for reading tag condition sections of xml files
     */
    private static class ConditionReader {

        String openingName;
        TagCondition condition;
        boolean finished;

        private final List<ConditionReader> childReaders = new LinkedList<>();
        private ConditionReader currentChildReader = null;

        public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {

            if (finished) {
                throw new SAXException(tr("Condition is already finished at <{0}>", name));
            }

            if (currentChildReader != null) {
                currentChildReader.startElement(uri, localName, name, attributes);
                return;
            }

            //first tag is start tag of this condition
            if (openingName == null) {

                openingName = name;

                if ("tag".equals(name)) {
                    condition = TagConditionLogic.tag(readTag(attributes));
                } else if ("key".equals(name)) {
                    condition = TagConditionLogic.key(readKey(attributes));
                } else if (!("or".equals(name)) && !("and".equals(name)) && !("not".equals(name))) {
                    throw new SAXException(tr("Unknown tag for condition: {0}", name));
                }

                //all tags after the first are start tags of child conditions
            } else {

                if ("tag".equals(openingName) || "key".equals(openingName)) {
                    throw new SAXException(tr("Element must not have children: {0}", openingName));
                }

                currentChildReader = new ConditionReader();
                currentChildReader.startElement(uri, localName, name, attributes);
            }
        }

        public void endElement(String uri, String localName, String name) throws SAXException {

            if (finished) {
                throw new SAXException(tr("Condition is already finished at </{0}>", name));
            }

            /* if active child reader exists, pass parameter to it. */
            if (currentChildReader != null) {

                currentChildReader.endElement(uri, localName, name);

                if (currentChildReader.isFinished()) {
                    childReaders.add(currentChildReader);
                    currentChildReader = null;
                }

            } else {

                if (openingName.equals(name)) {

                    List<TagCondition> childConditions = new ArrayList<>();
                    for (ConditionReader childReader : childReaders) {
                        childConditions.add(childReader.getCondition());
                    }

                    if ("and".equals(openingName)) {
                        if (childConditions.size() > 0) {
                            condition = TagConditionLogic.and(childConditions);
                        } else {
                            throw new SAXException(tr("<and> needs at least one child"));
                        }
                    } else if ("or".equals(openingName)) {
                        if (childConditions.size() > 0) {
                            condition = TagConditionLogic.or(childConditions);
                        } else {
                            throw new SAXException(tr("<or> needs at least one child"));
                        }
                    } else if ("not".equals(openingName)) {
                        if (childConditions.size() == 1) {
                            condition = TagConditionLogic.not(childConditions.get(0));
                        } else {
                            throw new SAXException(tr("<not> needs at least one child"));
                        }
                    }

                    finished = true;

                } else {
                    throw new SAXException(tr("Wrong closing tag {0} (</{1}> expected)", name, openingName));
                }
            }
        }

        public boolean isFinished() {
            return finished;
        }

        public TagCondition getCondition() {
            if (!finished) {
                throw new IllegalStateException(tr("Condition {0} not yet finished", openingName));
            } else {
                assert condition != null;
                return condition;
            }
        }
    }
}
