package net.ranides.assira.grammar.impl;

import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.LazyCaller;
import net.ranides.assira.generic.LazyCallers;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.grammar.G4Node;
import net.ranides.assira.grammar.properties.G4NodeProperties;
import net.ranides.assira.grammar.properties.G4NodeProperty;
import net.ranides.assira.reflection.IClass;

import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;

abstract class NodeAbstract implements G4Node {

    protected final RootAbstract root;

    protected final G4Node parent;

    protected final LazyCaller<String> path = LazyCallers.shared();

    protected final LazyCaller<String> pathComponent = LazyCallers.shared();

    public NodeAbstract(RootAbstract root, G4Node parent) {
        this.root = root;
        this.parent = parent;
    }

    @Override
    public final IClass<?> type() {
        return IClass.typefor(unwrap());
    }

    @Override
    public final G4Node root() {
        return root.tree();
    }

    @Override
    public CQuery<G4Node> children(String name) {
        if(name.equals(NodeSupport.PARENT_SELECTOR)) {
            return CQuery.from().nullable(parent);
        }
        if(name.startsWith(NodeSupport.LABEL_SELECTOR)) {
            return CQuery.from().optional(root.findLabel(this, name.substring(1)));
        }
        if(name.endsWith(NodeSupport.INDEX_SUFFIX)) {
            Matcher hit = NodeSupport.INDEX_SELECTOR.matcher(name);
            if(hit.matches()) {
                String n = hit.group(1);
                int i = Integer.parseInt(hit.group(2));
                children().filter(v -> n.equals(v.shortname()) || n.equals(v.qualifier()) || n.equals(v.name())).at(i);
            }
        }
        if(NodeSupport.POSITION_SELECTOR.matcher(name).matches()) {
            int i = Integer.parseInt(name);
            return CQuery.from().optional(children().at(i));
        }
        return children().filter(v -> name.equals(v.name()) || name.equals(v.qualifier()) || name.equals(v.shortname()));
    }

    @Override
    public G4Node child(int index) {
        return children().at(index).orElseThrow(() -> new NoSuchElementException("No child [" + index + "] inside " + this));
    }

    @Override
    public G4Node child(String name) {
        return children(name)
            .single(() -> new NoSuchElementException("More than one child '" + name + "' inside " + this))
            .orElseThrow(() -> new NoSuchElementException("No child '" + name + "' inside " + this));
    }

    @Override
    public CQuery<G4Node> findAll(String path) {
        String[] names = path.split("/");
        CQuery<G4Node> current = CQuery.from().value(this);

        current = current.message(() -> new NoSuchElementException("There is no path '"+path+"' inside node '"+this+"'"));

        for(String name : names) {
            current = current.flat(v -> v.children(name));
        }
        return current;
    }

    @Override
    public G4Node find(String path) {
        return findAll(path).first().get();
    }

    @Override
    public String name() {
        return shortname() + qualifier();
    }

    @Override
    public final Ref<G4Node> parent() {
        return Ref.ofNullable(parent);
    }

    @Override
    public Ref<G4Node> parent(int levels) {
        Ref<G4Node> out = parent();
        for(int i=1; i<levels; i++) {
            out = out.flatMap(o -> o.parent());
        }
        return out;
  }

    @Override
    public CQuery<G4Node> parents() {
        return CQuery.from().finite(this, n -> n.parent());
    }

    @Override
    public final String path() {
        return path.computeIfAbsent(this::ipath);
    }

    @Override
    public final String pathComponent() {
        return pathComponent.computeIfAbsent(this::ipathComponent);
    }

    private String ipath() {
        return parents().map(n -> n.pathComponent()).reverse().join("/");
    }

    private String ipathComponent() {
        Optional<Integer> index = getPathComponentIndex();
        if(index.isPresent()) {
            return shortname() + "[" + index.get() + "]";
        } else {
            return shortname();
        }
    }

    private Optional<Integer> getPathComponentIndex() {
        if (parent == null || parent.children().size() == 1) {
            return Optional.empty();
        }

        int found = 0;
        Optional<Integer> index = Optional.empty();

        for (G4Node child : parent.children()) {
            if (child.equals(this)) {
                index = Optional.of(found);

                // break unnecessary looping, indexes greater that zero are always displayed
                if (found > 0) {
                    return index;
                }
            }
            if (child.shortname().equals(this.shortname())) {
                found++;

                // break unnecessary looping, zero index should be displayed if there is more than one similar element
                if (found > 1 && index.isPresent()) {
                    break;
                }
            }
        }

        // do not return zero index, if there is only one (i.e. this) child
        return found > 1 ? index : Optional.empty();
    }

    @Override
    public boolean has(G4NodeProperty<?> key) {
        return G4NodeProperties.has(this, key);
    }

    @Override
    public boolean has(G4NodeProperty<?> key, Object value) {
        return G4NodeProperties.has(this, key, value);
    }

    @Override
    public <V> Ref<V> get(G4NodeProperty<V> key) {
        return G4NodeProperties.get(this, key);
    }

    @Override
    public boolean equals(Object o) {
        if(o instanceof NodeAbstract) {
            NodeAbstract that = (NodeAbstract)o;
            return this.unwrap().equals(that.unwrap());
        }

        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(unwrap());
    }

    @Override
    public String toString() {
        return path() + qualifier() + "=" + text();
    }
}
