package haskell.lang;

import haskell.prelude.Function;
import haskell.prelude.List;
import haskell.prelude.text.Show;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;

public abstract class AbstractData<A extends AbstractData<A>>
        implements Data<A>,
                Exp<A> {

    final java.lang.String $name;

    final Internal $internal;

    final Exp<A> $expression;

    A $eval;

    protected AbstractData() {
        this(null, new Internal());
    }

    protected AbstractData(final java.lang.String name) {
        this(name, new Internal());
    }

    @SuppressWarnings("unchecked")
    protected AbstractData(
            final java.lang.String name,
            final Internal internal) {
        this.$name = name;
        this.$internal = internal;
        this.$expression = null;
        this.$eval = (A) this;
    }

    protected AbstractData(final Exp<A> expression) {
        this.$name = null;
        this.$internal = null;
        this.$expression = expression;
        this.$eval = null;
    }

    protected java.lang.String $name() {
        if ($name != null) {
            return $name;
        } else {
            return getClass().getSimpleName();
        }
    }

    protected Internal $internal() {
        return $internal;
    }

    protected Exp<A> $expression() {
        return $expression;
    }

    static volatile int nAfterEvaluatings = 0;

    public A eval() {
        if ($eval == null) {
            $eval = evaluate();
            if (nAfterEvaluatings == 0) {
                ++nAfterEvaluatings;
                try {
                    afterEvaluate();
                } finally {
                    --nAfterEvaluatings;
                }
            }
        }
        return $eval;
    }

    /**
     * must override if $expression == null
     */
    protected A evaluate() {
        return $expression.eval();
    }

    void afterEvaluate() {
        System.err.println("[DEBUG] evaluate: "
                + this + " => " + description($eval));
    }

    protected static class Internal {

        @Override
        public int hashCode() {
            return $fieldMap().hashCode();
        }

        @Override
        public boolean equals(final Object other) {
            if (!(other instanceof Internal)) return false;
            try {
                for (Field field : getClass().getFields()) {
                    if (!isTargetField(field)) continue;
                    if (!equals(field.get(this), field.get(other))) {
                        return false;
                    }
                }
            } catch (final IllegalAccessException e) {
                throw new InternalError();
            }
            return true;
        }

        protected static boolean equals(final Object o1, final Object o2) {
            return AbstractData.equals(o1, o2);
        }

        @Override
        public java.lang.String toString() {
            final Map<java.lang.String, Object> map = $fieldMap();
            if (map.isEmpty()) {
                return getClass().getSimpleName();
            } else {
                return map.toString();
            }
        }

        protected java.lang.String $description(final java.lang.String name) {
            final Map<java.lang.String, Object> map = $fieldMap();
            if (map.isEmpty()) {
                return name;
            } else {
                return name + ' ' + map.toString();
            }
        }

        protected Map<java.lang.String, Object> $fieldMap() {
            return fieldMap(this);
        }

    }

    // ---- java.lang.Object

    @Override
    public int hashCode() {
        return eval().$fieldMap().hashCode();
    }

    @Override
    @SuppressWarnings("unchecked")
    public boolean equals(final Object other) {
        if (!(other instanceof AbstractData)) {
            return false;
        }
        try {
            return equals((A) other);
        } catch (final ClassCastException e) {
            return false;
        }
    }

    public boolean equals(final A other) {
        if (other == null) return false;
        final A x = eval();
        final A y = other.eval();
        if (x == y) return true;
        try {
            for (Field field : getClass().getFields()) {
                if (!isTargetField(field)) continue;
                if (!equals(field.get(x), field.get(y))) {
                    return false;
                }
            }
        } catch (final IllegalAccessException e) {
            throw new InternalError();
        }
        final Internal xi = x.$internal();
        final Internal yi = y.$internal();
        if (xi == yi) return true;
        return (xi != null) && xi.equals(yi);
    }

    protected static boolean equals(final Object o1, final Object o2) {
        if (o1 == o2) return true;
        return (o1 != null && o1.equals(o2));
    }

    @Override
    public java.lang.String toString() {
        if ($expression != null) {
            return $expression.toString();
        } else {
            return $description();
        }
    }

    protected java.lang.String $description() {
        final Map<java.lang.String, Object> map = $fieldMap();
        if (map.isEmpty()) {
            return $name();
        } else {
            for (Map.Entry<java.lang.String, Object> entry : map.entrySet()) {
                entry.setValue(description(entry.getValue()));
            }
            return $name() + ' ' + map;
        }
    }

    static java.lang.String description(final Object o) {
        if (o == null) {
            return null;
        } else if (o instanceof Show<?>) {
            return ((Show<?>) o)._show_().toString();
        } else {
            return o.toString();
        }
    }

    protected Map<java.lang.String, Object> $fieldMap() {
        final Map<java.lang.String, Object> map = fieldMap(this);
        if ($internal != null) {
            map.putAll(fieldMap($internal));
        }
        return map;
    }

    protected static Map<java.lang.String, Object> fieldMap(final Object o) {
        final Map<java.lang.String, Object> map
                = new LinkedHashMap<java.lang.String, Object>();
        try {
            for (Field field : o.getClass().getFields()) {
                if (!isTargetField(field)) continue;
                final java.lang.String name = field.getName();
                map.put(name, field.get(o));
            }
        } catch (final IllegalAccessException e) {
            throw new InternalError();
        }
        return map;
    }

    protected static boolean isTargetField(final Field field) {
        final int mod = field.getModifiers();
        if (Modifier.isStatic(mod)) return false;
        final java.lang.String name = field.getName();
        if (name.startsWith("$")) return false;
        return true;
    }

    // ---- data a -> b

    /**
     * this `f`
     */
    @SuppressWarnings("unchecked")
    public final <B> B apply(final Function<A, B> f) {
        return f.apply((A) this);
    }

    /**
     * this `f` x
     */
    @SuppressWarnings("unchecked")
    public final <B, C> C apply(
            final Function<A, Function<B, C>> f, final B x) {
        return f.apply((A) this, x);
    }

    // ---- data [a]

    /**
     * this : tail
     */
    @SuppressWarnings("unchecked")
    public List<A> _cons_(final List<A> tail) {
        return new List<A>((A) this, tail);
    }

}
