/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ignite.internal.processors.cache.version;

import org.apache.ignite.internal.processors.cache.CacheObjectValueContext;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.tostring.GridToStringInclude;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.jetbrains.annotations.Nullable;

/**
 * Conflict context implementation.
 */
public class GridCacheVersionConflictContext<K, V> {
    /** Old entry. */
    @GridToStringInclude
    private final GridCacheVersionedEntry<K, V> oldEntry;

    /** New entry. */
    @GridToStringInclude
    private final GridCacheVersionedEntry<K, V> newEntry;

    /** Object context. */
    private final CacheObjectValueContext ctx;

    /** Current state. */
    private State state;

    /** Current merge value. */
    @GridToStringExclude
    private V mergeVal;

    /** TTL. */
    private long ttl;

    /** Expire time. */
    private long expireTime;

    /** Manual resolve flag. */
    private boolean manualResolve;

    /**
     * Constructor.
     *
     * @param oldEntry Old entry.
     * @param newEntry New entry.
     */
    public GridCacheVersionConflictContext(CacheObjectValueContext ctx, GridCacheVersionedEntry<K, V> oldEntry,
        GridCacheVersionedEntry<K, V> newEntry) {
        assert oldEntry != null && newEntry != null;
        assert oldEntry.ttl() >= 0 && newEntry.ttl() >= 0;

        this.oldEntry = oldEntry;
        this.newEntry = newEntry;
        this.ctx = ctx;

        // Set initial state.
        useNew();
    }

    /**
     * Gets old (existing) cache entry.
     *
     * @return Old (existing) cache entry.
     */
    public GridCacheVersionedEntry<K, V> oldEntry() {
        return oldEntry;
    }

    /**
     * Gets new cache entry.
     *
     * @return New cache entry.
     */
    public GridCacheVersionedEntry<K, V> newEntry() {
        return newEntry;
    }

    /**
     * Gets cache object context.
     *
     * @return Cache object context.
     */
    public CacheObjectValueContext valueContext() {
        return ctx;
    }

    /**
     * Force cache to ignore new entry and leave old (existing) entry unchanged.
     */
    public void useOld() {
        state = State.USE_OLD;
    }

    /**
     * Force cache to apply new entry overwriting old (existing) entry.
     */
    public void useNew() {
        state = State.USE_NEW;

        ttl = newEntry.ttl();
    }

    /**
     * Force cache to use neither old, nor new, but some other value passed as argument. In this case old
     * value will be replaced with merge value and update will be considered as local.
     * <p>
     * Also in case of merge you have to specify new TTL and expire time explicitly. For unlimited TTL use {@code 0}.
     *
     * @param mergeVal Merge value or {@code null} to force remove.
     * @param ttl Time to live in milliseconds (must be non-negative).
     * @param expireTime Expire time.
     */
    public void merge(@Nullable V mergeVal, long ttl, long expireTime) {
        if (ttl < 0)
            throw new IllegalArgumentException("TTL must be non-negative: " + ttl);

        state = State.MERGE;

        this.mergeVal = mergeVal;
        this.ttl = ttl;
        this.expireTime = expireTime;
    }

    /**
     * @return {@code True} in case old value should be used.
     */
    public boolean isUseOld() {
        return state == State.USE_OLD;
    }

    /**
     * @return {@code True} in case new value should be used.
     */
    public boolean isUseNew() {
        return state == State.USE_NEW;
    }

    /**
     * @return {@code True} in case merge is to be performed.
     */
    public boolean isMerge() {
        return state == State.MERGE;
    }

    /**
     * Set manual resolve class.
     */
    public void manualResolve() {
        this.manualResolve = true;
    }

    /**
     * @return Manual resolve flag.
     */
    public boolean isManualResolve() {
        return manualResolve;
    }

    /**
     * @return Value to merge (if any).
     */
    @Nullable public V mergeValue() {
        return mergeVal;
    }

    /**
     * @return TTL.
     */
    public long ttl() {
        return ttl;
    }

    /**
     * @return Expire time.
     */
    public long expireTime() {
        return isUseNew() ? newEntry.expireTime() : isUseOld() ? oldEntry.expireTime() : expireTime;
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return state == State.MERGE ?
            S.toString(GridCacheVersionConflictContext.class, this, "mergeValue", mergeVal, true) :
            S.toString(GridCacheVersionConflictContext.class, this);
    }

    /**
     * State.
     */
    private enum State {
        /** Use old. */
        USE_OLD,

        /** Use new. */
        USE_NEW,

        /** Merge. */
        MERGE
    }
}
