// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.tools.template_engine;

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

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

import org.openstreetmap.josm.actions.search.SearchCompiler.And;
import org.openstreetmap.josm.actions.search.SearchCompiler.Child;
import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
import org.openstreetmap.josm.actions.search.SearchCompiler.Not;
import org.openstreetmap.josm.actions.search.SearchCompiler.Or;
import org.openstreetmap.josm.actions.search.SearchCompiler.Parent;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;

public class ContextSwitchTemplate implements TemplateEntry {

    private static final TemplateEngineDataProvider EMTPY_PROVIDER = new TemplateEngineDataProvider() {
        @Override
        public Object getTemplateValue(String name, boolean special) {
            return null;
        }

        @Override
        public Collection<String> getTemplateKeys() {
            return Collections.emptyList();
        }

        @Override
        public boolean evaluateCondition(Match condition) {
            return false;
        }
    };

    private abstract class ContextProvider extends Match {
        Match condition;
        abstract List<OsmPrimitive> getPrimitives(OsmPrimitive root);
    }

    private class ParentSet extends ContextProvider {
        private final Match childCondition;

        ParentSet(Match child) {
            this.childCondition = child;
        }
        @Override
        public boolean match(OsmPrimitive osm) {
            throw new UnsupportedOperationException();
        }
        @Override
        List<OsmPrimitive> getPrimitives(OsmPrimitive root) {
            List<OsmPrimitive> children;
            if (childCondition instanceof ContextProvider) {
                children = ((ContextProvider) childCondition).getPrimitives(root);
            } else if (childCondition.match(root)) {
                children = Collections.singletonList(root);
            } else {
                children = Collections.emptyList();
            }

            List<OsmPrimitive> result = new ArrayList<>();
            for (OsmPrimitive child: children) {
                for (OsmPrimitive parent: child.getReferrers(true)) {
                    if (condition == null || condition.match(parent)) {
                        result.add(parent);
                    }
                }
            }
            return result;
        }
    }

    private class ChildSet extends ContextProvider {
        private final Match parentCondition;

        ChildSet(Match parentCondition) {
            this.parentCondition = parentCondition;
        }

        @Override
        public boolean match(OsmPrimitive osm) {
            throw new UnsupportedOperationException();
        }

        @Override
        List<OsmPrimitive> getPrimitives(OsmPrimitive root) {
            List<OsmPrimitive> parents;
            if (parentCondition instanceof ContextProvider) {
                parents = ((ContextProvider) parentCondition).getPrimitives(root);
            } else if (parentCondition.match(root)) {
                parents = Collections.singletonList(root);
            } else {
                parents = Collections.emptyList();
            }
            List<OsmPrimitive> result = new ArrayList<>();
            for (OsmPrimitive p: parents) {
                if (p instanceof Way) {
                    for (Node n: ((Way) p).getNodes()) {
                        if (condition != null && condition.match(n)) {
                            result.add(n);
                        }
                        result.add(n);
                    }
                } else if (p instanceof Relation) {
                    for (RelationMember rm: ((Relation) p).getMembers()) {
                        if (condition != null && condition.match(rm.getMember())) {
                            result.add(rm.getMember());
                        }
                    }
                }
            }
            return result;
        }
    }

    private class OrSet extends ContextProvider {
        private final ContextProvider lhs;
        private final ContextProvider rhs;

        OrSet(ContextProvider lhs, ContextProvider rhs) {
            this.lhs = lhs;
            this.rhs = rhs;
        }

        @Override
        public boolean match(OsmPrimitive osm) {
            throw new UnsupportedOperationException();
        }

        @Override
        List<OsmPrimitive> getPrimitives(OsmPrimitive root) {
            List<OsmPrimitive> result = new ArrayList<>();
            for (OsmPrimitive o: lhs.getPrimitives(root)) {
                if (condition == null || condition.match(o)) {
                    result.add(o);
                }
            }
            for (OsmPrimitive o: rhs.getPrimitives(root)) {
                if (condition == null || condition.match(o) && !result.contains(o)) {
                    result.add(o);
                }
            }
            return result;
        }
    }

    private class AndSet extends ContextProvider {
        private final ContextProvider lhs;
        private final ContextProvider rhs;

        AndSet(ContextProvider lhs, ContextProvider rhs) {
            this.lhs = lhs;
            this.rhs = rhs;
        }

        @Override
        public boolean match(OsmPrimitive osm) {
            throw new UnsupportedOperationException();
        }

        @Override
        List<OsmPrimitive> getPrimitives(OsmPrimitive root) {
            List<OsmPrimitive> result = new ArrayList<>();
            List<OsmPrimitive> lhsList = lhs.getPrimitives(root);
            for (OsmPrimitive o: rhs.getPrimitives(root)) {
                if (lhsList.contains(o) && (condition == null || condition.match(o))) {
                    result.add(o);
                }
            }
            return result;
        }
    }

    private final ContextProvider context;
    private final TemplateEntry template;

    private Match transform(Match m, int searchExpressionPosition) throws ParseError {
        if (m instanceof Parent) {
            Match child = transform(((Parent) m).getOperand(), searchExpressionPosition);
            return new ParentSet(child);
        } else if (m instanceof Child) {
            Match parent = transform(((Child) m).getOperand(), searchExpressionPosition);
            return new ChildSet(parent);
        } else if (m instanceof And) {
            Match lhs = transform(((And) m).getLhs(), searchExpressionPosition);
            Match rhs = transform(((And) m).getRhs(), searchExpressionPosition);

            if (lhs instanceof ContextProvider && rhs instanceof ContextProvider)
                return new AndSet((ContextProvider)lhs, (ContextProvider)rhs);
            else if (lhs instanceof ContextProvider) {
                ContextProvider cp = (ContextProvider) lhs;
                if (cp.condition == null) {
                    cp.condition = rhs;
                } else {
                    cp.condition = new And(cp.condition, rhs);
                }
                return cp;
            } else if (rhs instanceof ContextProvider) {
                ContextProvider cp = (ContextProvider) rhs;
                if (cp.condition == null) {
                    cp.condition = lhs;
                } else {
                    cp.condition = new And(lhs, cp.condition);
                }
                return cp;
            } else
                return m;
        } else if (m instanceof Or) {
            Match lhs = transform(((Or) m).getLhs(), searchExpressionPosition);
            Match rhs = transform(((Or) m).getRhs(), searchExpressionPosition);

            if (lhs instanceof ContextProvider && rhs instanceof ContextProvider)
                return new OrSet((ContextProvider)lhs, (ContextProvider)rhs);
            else if (lhs instanceof ContextProvider)
                throw new ParseError(tr("Error in search expression on position {0} - right side of or(|) expression must return set of primitives", searchExpressionPosition));
            else if (rhs instanceof ContextProvider)
                throw new ParseError(tr("Error in search expression on position {0} - left side of or(|) expression must return set of primitives", searchExpressionPosition));
            else
                return m;
        } else if (m instanceof Not) {
            Match match = transform(((Not) m).getMatch(), searchExpressionPosition);
            if (match instanceof ContextProvider)
                throw new ParseError(tr("Error in search expression on position {0} - not(-) cannot be used in this context", searchExpressionPosition));
            else
                return m;
        } else
            return m;
    }

    public ContextSwitchTemplate(Match match, TemplateEntry template, int searchExpressionPosition) throws ParseError {
        Match m = transform(match, searchExpressionPosition);
        if (!(m instanceof ContextProvider))
            throw new ParseError(tr("Error in search expression on position {0} - expression must return different then current primitive", searchExpressionPosition));
        else {
            context = (ContextProvider) m;
        }
        this.template = template;
    }

    @Override
    public void appendText(StringBuilder result, TemplateEngineDataProvider dataProvider) {
        List<OsmPrimitive> primitives = context.getPrimitives((OsmPrimitive) dataProvider);
        if (primitives != null && !primitives.isEmpty()) {
            template.appendText(result, primitives.get(0));
        } else {
            template.appendText(result, EMTPY_PROVIDER);
        }
    }

    @Override
    public boolean isValid(TemplateEngineDataProvider dataProvider) {
        List<OsmPrimitive> primitives = context.getPrimitives((OsmPrimitive) dataProvider);
        if (primitives != null && !primitives.isEmpty())
            return template.isValid(primitives.get(0));
        else
            return false;
    }

}
