package com.googlecode.totallylazy.collections;

import com.googlecode.totallylazy.functions.Function1;
import com.googlecode.totallylazy.functions.Functions;
import com.googlecode.totallylazy.Option;
import com.googlecode.totallylazy.Pair;
import com.googlecode.totallylazy.annotations.tailrec;

import static com.googlecode.totallylazy.Option.none;
import static com.googlecode.totallylazy.Option.some;
import static com.googlecode.totallylazy.predicates.Predicates.is;
import static com.googlecode.totallylazy.predicates.Predicates.where;
import static com.googlecode.totallylazy.Sequences.sequence;
import static com.googlecode.totallylazy.collections.TreeZipper.Breadcrumb.breadcrumb;
import static com.googlecode.totallylazy.collections.TreeZipper.Direction.left;
import static com.googlecode.totallylazy.collections.TreeZipper.Direction.right;
import static com.googlecode.totallylazy.collections.TreeZipper.functions.direction;
import static java.lang.String.format;

public class TreeZipper<K, V> implements Zipper<Pair<K, V>> {
    public final TreeMap<K, V> focus;
    public final PersistentList<Breadcrumb<K, V>> breadcrumbs;

    private TreeZipper(TreeMap<K, V> focus, PersistentList<Breadcrumb<K, V>> breadcrumbs) {
        this.focus = focus;
        this.breadcrumbs = breadcrumbs;
    }

    public static <K, V> TreeZipper<K, V> zipper(TreeMap<K, V> focus) {
        return new TreeZipper<K, V>(focus, PersistentList.constructors.<Breadcrumb<K, V>>empty());
    }

    private static <K, V> TreeZipper<K, V> zipper(TreeMap<K, V> focus, PersistentList<Breadcrumb<K, V>> crumbs) {
        return new TreeZipper<K, V>(focus, crumbs);
    }

    public TreeZipper<K, V> left() {
        return zipper(focus.left(), breadcrumbs.cons(breadcrumb(left, focus.head(), focus.right())));
    }

    public TreeZipper<K, V> right() {
        return zipper(focus.right(), breadcrumbs.cons(breadcrumb(right, focus.head(), focus.left())));
    }

    public TreeZipper<K, V> up() {
        final Breadcrumb<K, V> breadcrumb = breadcrumbs.head();
        final TreeMap<K, V> left = breadcrumb.direction == Direction.left ? focus : breadcrumb.other;
        final TreeMap<K, V> right = breadcrumb.direction == Direction.left ? breadcrumb.other : focus;
        return zipper(focus.factory().create(focus.comparator(), breadcrumb.parent.first(), breadcrumb.parent.second(), left, right), breadcrumbs.tail());
    }

    @tailrec
    public TreeZipper<K, V> top() {
        if (breadcrumbs.isEmpty()) return this;
        return up().top();
    }

    public TreeMap<K, V> toTreeMap() {
        return top().focus;
    }

    public TreeZipper<K, V> modify(Function1<? super TreeMap<K, V>, ? extends TreeMap<K, V>> callable) {
        TreeMap<K, V> result = Functions.call(callable, focus);
        TreeZipper<K, V> newZipper = zipper(result, breadcrumbs);
        if (newZipper.focus.isEmpty()) return newZipper.up();
        return newZipper;
    }

    public TreeZipper<K, V> replace(K key, V value) {
        return modify(functions.replace(key, value));
    }

    public TreeZipper<K, V> delete() {
        return remove();
    }

    public TreeZipper<K, V> remove() {
        return modify(functions.<K, V>remove());
    }

    @tailrec
    public TreeZipper<K, V> first() {
        if (isFirst()) return this;
        return left().first();
    }

    @tailrec
    public TreeZipper<K, V> last() {
        if (isLast()) return this;
        return right().last();
    }

    @Override
    public boolean isFirst() {
        return focus.left().isEmpty();
    }

    @Override
    public boolean isLast() {
        return focus.right().isEmpty();
    }

    @Override
    public int index() {
        return focus.indexOf(value()) + breadcrumbs.toSequence().
                filter(where(direction, is(right))).
                fold(0, (integer, breadcrumb) -> integer + breadcrumb.other.size() + 1);
    }

    @Override
    public TreeZipper<K, V> index(int index) {
        int position = index();
        if (position == index) return this;
        if (position < index) return next().index(index);
        return previous().index(index);
    }

    public boolean isTop() {
        return breadcrumbs.isEmpty();
    }

    public TreeZipper<K, V> next() {
        if (focus.right().isEmpty()) return backtrack(right).up();
        return right().first();
    }

    public Option<TreeZipper<K, V>> nextOption() {
        try {
            return some(next());
        } catch (Exception e) {
            return none();
        }
    }

    public TreeZipper<K, V> previous() {
        if (focus.left().isEmpty()) return backtrack(left).up();
        return left().last();
    }

    public Option<TreeZipper<K, V>> previousOption() {
        try {
            return some(previous());
        } catch (Exception e) {
            return none();
        }
    }

    @tailrec
    private TreeZipper<K, V> backtrack(final Direction direction) {
        if (breadcrumbs.head().direction.equals(direction)) return up().backtrack(direction);
        return this;
    }

    @Override
    public String toString() {
        return "TreeZipper{" +
                "focus=" + focus +
                ", breadcrumbs=" + breadcrumbs +
                '}';
    }

    public Pair<K, V> pair() {
        return Pair.pair(focus.key(), focus.value());
    }

    @Override
    public Pair<K, V> value() {
        return focus.head();
    }

    public enum Direction {
        left, right
    }

    public static final class Breadcrumb<K, V> {
        public final Direction direction;
        public final Pair<K, V> parent;
        public final TreeMap<K, V> other;

        private Breadcrumb(Direction direction, Pair<K, V> parent, TreeMap<K, V> other) {
            this.parent = parent;
            this.other = other;
            this.direction = direction;
        }

        public static <K, V> Breadcrumb<K, V> breadcrumb(Direction direction, Pair<K, V> parent, TreeMap<K, V> other) {
            return new Breadcrumb<K, V>(direction, parent, other);
        }

        @Override
        public String toString() {
            return format("direction(%s), parent(%s), other(%s)", direction, parent, other);
        }

        @Override
        public int hashCode() {
            return sequence(direction, parent, other).hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof Breadcrumb &&
                    ((Breadcrumb) obj).direction.equals(direction) &&
                    ((Breadcrumb) obj).parent.equals(parent) &&
                    ((Breadcrumb) obj).other.equals(other);
        }
    }

    public static class functions extends TreeMap.functions {
        public static Function1<Breadcrumb<?, ?>, Direction> direction = breadcrumb -> breadcrumb.direction;
    }

}