/**
 *   Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com>
 */
package org.deephacks.confit.internal.core.property.typesafe.impl;

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

import org.deephacks.confit.internal.core.property.typesafe.ConfigMergeable;
import org.deephacks.confit.internal.core.property.typesafe.ConfigRenderOptions;
import org.deephacks.confit.internal.core.property.typesafe.ConfigException;
import org.deephacks.confit.internal.core.property.typesafe.ConfigObject;
import org.deephacks.confit.internal.core.property.typesafe.ConfigOrigin;
import org.deephacks.confit.internal.core.property.typesafe.ConfigValue;

/**
 *
 * Trying very hard to avoid a parent reference in typesafe values; when you have
 * a tree like this, the availability of parent() tends to result in a lot of
 * improperly-factored and non-modular code. Please don't add parent().
 *
 */
abstract class AbstractConfigValue implements ConfigValue, MergeableValue {

    final private SimpleConfigOrigin origin;

    AbstractConfigValue(ConfigOrigin origin) {
        this.origin = (SimpleConfigOrigin) origin;
    }

    @Override
    public SimpleConfigOrigin origin() {
        return this.origin;
    }

    /**
     * This exception means that a value is inherently not resolveable, at the
     * moment the only known cause is a cycle of substitutions. This is a
     * checked exception since it's internal to the library and we want to be
     * sure we handle it before passing it out to public API. This is only
     * supposed to be thrown by the target of a cyclic reference and it's
     * supposed to be caught by the ConfigReference looking up that reference,
     * so it should be impossible for an outermost resolve() to throw this.
     *
     * Contrast with ConfigException.NotResolved which just means nobody called
     * resolve().
     */
    static class NotPossibleToResolve extends Exception {
        private static final long serialVersionUID = 1L;

        final private String traceString;

        NotPossibleToResolve(ResolveContext context) {
            super("was not possible to resolve");
            this.traceString = context.traceString();
        }

        String traceString() {
            return traceString;
        }
    }

    /**
     * Called only by ResolveContext.resolve().
     *
     * @param context
     *            state of the current resolve
     * @return a new value if there were changes, or this if no changes
     */
    AbstractConfigValue resolveSubstitutions(ResolveContext context)
            throws NotPossibleToResolve {
        return this;
    }

    ResolveStatus resolveStatus() {
        return ResolveStatus.RESOLVED;
    }

    /**
     * This is used when including one file in another; the included file is
     * relativized to the path it's included into in the parent file. The point
     * is that if you include a file at foo.bar in the parent, and the included
     * file as a substitution ${a.b.c}, the included substitution now needs to
     * be ${foo.bar.a.b.c} because we resolve substitutions globally only after
     * parsing everything.
     *
     * @param prefix
     * @return value relativized to the given path or the same value if nothing
     *         to do
     */
    AbstractConfigValue relativized(Path prefix) {
        return this;
    }

    protected interface Modifier {
        // keyOrNull is null for non-objects
        AbstractConfigValue modifyChildMayThrow(String keyOrNull, AbstractConfigValue v)
                throws Exception;
    }

    protected abstract class NoExceptionsModifier implements Modifier {
        @Override
        public final AbstractConfigValue modifyChildMayThrow(String keyOrNull, AbstractConfigValue v)
                throws Exception {
            try {
                return modifyChild(keyOrNull, v);
            } catch (RuntimeException e) {
                throw e;
            } catch(Exception e) {
                throw new ConfigException.BugOrBroken("Unexpected exception", e);
            }
        }

        abstract AbstractConfigValue modifyChild(String keyOrNull, AbstractConfigValue v);
    }

    @Override
    public AbstractConfigValue toFallbackValue() {
        return this;
    }

    protected abstract AbstractConfigValue newCopy(ConfigOrigin origin);

    // this is virtualized rather than a field because only some subclasses
    // really need to store the boolean, and they may be able to pack it
    // with another boolean to save space.
    protected boolean ignoresFallbacks() {
        // if we are not resolved, then somewhere in this value there's
        // a substitution that may need to look at the fallbacks.
        return resolveStatus() == ResolveStatus.RESOLVED;
    }

    protected AbstractConfigValue withFallbacksIgnored() {
        if (ignoresFallbacks())
            return this;
        else
            throw new ConfigException.BugOrBroken(
                    "value class doesn't implement forced fallback-ignoring " + this);
    }

    // the withFallback() implementation is supposed to avoid calling
    // mergedWith* if we're ignoring fallbacks.
    protected final void requireNotIgnoringFallbacks() {
        if (ignoresFallbacks())
            throw new ConfigException.BugOrBroken(
                    "method should not have been called with ignoresFallbacks=true "
                            + getClass().getSimpleName());
    }

    protected AbstractConfigValue constructDelayedMerge(ConfigOrigin origin,
            List<AbstractConfigValue> stack) {
        return new ConfigDelayedMerge(origin, stack);
    }

    protected final AbstractConfigValue mergedWithTheUnmergeable(
            Collection<AbstractConfigValue> stack, Unmergeable fallback) {
        requireNotIgnoringFallbacks();

        // if we turn out to be an object, and the fallback also does,
        // then a merge may be required; delay until we resolve.
        List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
        newStack.addAll(stack);
        newStack.addAll(fallback.unmergedValues());
        return constructDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack);
    }

    private final AbstractConfigValue delayMerge(Collection<AbstractConfigValue> stack,
            AbstractConfigValue fallback) {
        // if we turn out to be an object, and the fallback also does,
        // then a merge may be required.
        // if we contain a substitution, resolving it may need to look
        // back to the fallback.
        List<AbstractConfigValue> newStack = new ArrayList<AbstractConfigValue>();
        newStack.addAll(stack);
        newStack.add(fallback);
        return constructDelayedMerge(AbstractConfigObject.mergeOrigins(newStack), newStack);
    }

    protected final AbstractConfigValue mergedWithObject(Collection<AbstractConfigValue> stack,
            AbstractConfigObject fallback) {
        requireNotIgnoringFallbacks();

        if (this instanceof AbstractConfigObject)
            throw new ConfigException.BugOrBroken("Objects must reimplement mergedWithObject");

        return mergedWithNonObject(stack, fallback);
    }

    protected final AbstractConfigValue mergedWithNonObject(Collection<AbstractConfigValue> stack,
            AbstractConfigValue fallback) {
        requireNotIgnoringFallbacks();

        if (resolveStatus() == ResolveStatus.RESOLVED) {
            // falling back to a non-object doesn't merge anything, and also
            // prohibits merging any objects that we fall back to later.
            // so we have to switch to ignoresFallbacks mode.
            return withFallbacksIgnored();
        } else {
            // if unresolved, we may have to look back to fallbacks as part of
            // the resolution process, so always delay
            return delayMerge(stack, fallback);
        }
    }

    protected AbstractConfigValue mergedWithTheUnmergeable(Unmergeable fallback) {
        requireNotIgnoringFallbacks();

        return mergedWithTheUnmergeable(Collections.singletonList(this), fallback);
    }

    protected AbstractConfigValue mergedWithObject(AbstractConfigObject fallback) {
        requireNotIgnoringFallbacks();

        return mergedWithObject(Collections.singletonList(this), fallback);
    }

    protected AbstractConfigValue mergedWithNonObject(AbstractConfigValue fallback) {
        requireNotIgnoringFallbacks();

        return mergedWithNonObject(Collections.singletonList(this), fallback);
    }

    public AbstractConfigValue withOrigin(ConfigOrigin origin) {
        if (this.origin == origin)
            return this;
        else
            return newCopy(origin);
    }

    // this is only overridden to change the return type
    @Override
    public AbstractConfigValue withFallback(ConfigMergeable mergeable) {
        if (ignoresFallbacks()) {
            return this;
        } else {
            ConfigValue other = ((MergeableValue) mergeable).toFallbackValue();

            if (other instanceof Unmergeable) {
                return mergedWithTheUnmergeable((Unmergeable) other);
            } else if (other instanceof AbstractConfigObject) {
                return mergedWithObject((AbstractConfigObject) other);
            } else {
                return mergedWithNonObject((AbstractConfigValue) other);
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof ConfigValue;
    }

    @Override
    public boolean equals(Object other) {
        // note that "origin" is deliberately NOT part of equality
        if (other instanceof ConfigValue) {
            return canEqual(other)
                    && (this.valueType() ==
                            ((ConfigValue) other).valueType())
                    && ConfigImplUtil.equalsHandlingNull(this.unwrapped(),
                            ((ConfigValue) other).unwrapped());
        } else {
            return false;
        }
    }

    @Override
    public int hashCode() {
        // note that "origin" is deliberately NOT part of equality
        Object o = this.unwrapped();
        if (o == null)
            return 0;
        else
            return o.hashCode();
    }

    @Override
    public final String toString() {
        StringBuilder sb = new StringBuilder();
        render(sb, 0, null /* atKey */, ConfigRenderOptions.concise());
        return getClass().getSimpleName() + "(" + sb.toString() + ")";
    }

    protected static void indent(StringBuilder sb, int indent, ConfigRenderOptions options) {
        if (options.getFormatted()) {
            int remaining = indent;
            while (remaining > 0) {
                sb.append("    ");
                --remaining;
            }
        }
    }

    protected void render(StringBuilder sb, int indent, String atKey, ConfigRenderOptions options) {
        if (atKey != null) {
            String renderedKey;
            if (options.getJson())
                renderedKey = ConfigImplUtil.renderJsonString(atKey);
            else
                renderedKey = ConfigImplUtil.renderStringUnquotedIfPossible(atKey);

            sb.append(renderedKey);

            if (options.getJson()) {
                if (options.getFormatted())
                    sb.append(" : ");
                else
                    sb.append(":");
            } else {
                // in non-JSON we can omit the colon or equals before an object
                if (this instanceof ConfigObject) {
                    if (options.getFormatted())
                        sb.append(' ');
                } else {
                    sb.append("=");
                }
            }
        }
        render(sb, indent, options);
    }

    protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) {
        Object u = unwrapped();
        sb.append(u.toString());
    }

    @Override
    public final String render() {
        return render(ConfigRenderOptions.defaults());
    }

    @Override
    public final String render(ConfigRenderOptions options) {
        StringBuilder sb = new StringBuilder();
        render(sb, 0, null, options);
        return sb.toString();
    }

    // toString() is a debugging-oriented string but this is defined
    // to create a string that would parse back to the value in JSON.
    // It only works for primitive values (that would be a single token)
    // which are auto-converted to strings when concatenating with
    // other strings or by the DefaultTransformer.
    String transformToString() {
        return null;
    }

    SimpleConfig atKey(ConfigOrigin origin, String key) {
        Map<String, AbstractConfigValue> m = Collections.singletonMap(key, this);
        return (new SimpleConfigObject(origin, m)).toConfig();
    }

    @Override
    public SimpleConfig atKey(String key) {
        return atKey(SimpleConfigOrigin.newSimple("atKey(" + key + ")"), key);
    }

    SimpleConfig atPath(ConfigOrigin origin, Path path) {
        Path parent = path.parent();
        SimpleConfig result = atKey(origin, path.last());
        while (parent != null) {
            String key = parent.last();
            result = result.atKey(origin, key);
            parent = parent.parent();
        }
        return result;
    }

    @Override
    public SimpleConfig atPath(String pathExpression) {
        SimpleConfigOrigin origin = SimpleConfigOrigin.newSimple("atPath(" + pathExpression + ")");
        return atPath(origin, Path.newPath(pathExpression));
    }
}
