/*
 * 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.cache.affinity;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import org.apache.ignite.internal.processors.cache.GridCacheDefaultAffinityKeyMapper;
import org.apache.ignite.internal.util.tostring.GridToStringInclude;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.S;

/**
 * Optional wrapper for cache keys to provide support
 * for custom affinity mapping. The value returned by
 * {@link #affinityKey(Object)} method will be used for key-to-node
 * affinity.
 * <p>
 * Note that the {@link #equals(Object)} and {@link #hashCode()} methods
 * delegate directly to the wrapped cache key provided by {@link #key()}
 * method.
 * <p>
 * This class is optional and does not have to be used. It only provides
 * extra convenience whenever custom affinity mapping is required. Here is
 * an example of how {@code Person} objects can be collocated with
 * {@code Company} objects they belong to:
 * <pre name="code" class="java">
 * Object personKey = new AffinityKey(myPersonId, myCompanyId);
 *
 * // Both, the company and the person objects will be cached on the same node.
 * cache.put(myCompanyId, new Company(..));
 * cache.put(personKey, new Person(..));
 * </pre>
 * <p>
 * For more information and examples of cache affinity refer to
 * {@link AffinityKeyMapper} and {@link AffinityKeyMapped @AffinityKeyMapped}
 * documentation.
 * @see AffinityKeyMapped
 * @see AffinityKeyMapper
 * @see AffinityFunction
 */
public class AffinityKey<K> implements Externalizable {
    /** */
    private static final long serialVersionUID = 0L;

    /** Key. */
    @GridToStringInclude(sensitive = true)
    private K key;

    /** Affinity key. */
    @AffinityKeyMapped
    @GridToStringInclude(sensitive = true)
    private Object affKey;

    /**
     * Empty constructor.
     */
    public AffinityKey() {
        // No-op.
    }

    /**
     * Initializes key wrapper for a given key. If affinity key
     * is not initialized, then this key will be used for affinity.
     *
     * @param key Key.
     */
    public AffinityKey(K key) {
        A.notNull(key, "key");

        this.key = key;
    }

    /**
     * Initializes key together with its affinity key counter-part.
     *
     * @param key Key.
     * @param affKey Affinity key.
     */
    public AffinityKey(K key, Object affKey) {
        A.notNull(key, "key");

        this.key = key;
        this.affKey = affKey;
    }

    /**
     * Gets wrapped key.
     *
     * @return Wrapped key.
     */
    public K key() {
        return key;
    }

    /**
     * Sets wrapped key.
     *
     * @param key Wrapped key.
     */
    public void key(K key) {
        this.key = key;
    }

    /**
     * Gets affinity key to use for affinity mapping. If affinity key is not provided,
     * then {@code key} value will be returned.
     * <p>
     * This method is annotated with {@link AffinityKeyMapped} and will be picked up
     * by {@link GridCacheDefaultAffinityKeyMapper} automatically.
     *
     * @param <T> Type of affinity key.
     *
     * @return Affinity key to use for affinity mapping.
     */
    public <T> T affinityKey() {
        A.notNull(key, "key");

        return (T)(affKey == null ? key : affKey);
    }

    /**
     * Sets affinity key to use for affinity mapping. If affinity key is not provided,
     * then {@code key} value will be returned.
     *
     * @param affKey Affinity key to use for affinity mapping.
     */
    public void affinityKey(Object affKey) {
        this.affKey = affKey;
    }

    /** {@inheritDoc} */
    @Override public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(key);
        out.writeObject(affKey);
    }

    /** {@inheritDoc} */
    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        key = (K)in.readObject();
        affKey = in.readObject();
    }

    /**
     * Hash code implementation which delegates to the underlying {@link #key()}. Note, however,
     * that different subclasses of {@link AffinityKey} will produce different hash codes.
     * <p>
     * Users should override this method if different behavior is desired.
     *
     * @return Hash code.
     */
    @Override public int hashCode() {
        A.notNull(key, "key");

        return 31 * key.hashCode() + getClass().getName().hashCode();
    }

    /**
     * Equality check which delegates to the underlying key equality. Note, however, that
     * different subclasses of {@link AffinityKey} will never be equal.
     * <p>
     * Users should override this method if different behavior is desired.
     *
     * @param obj Object to check for equality.
     * @return {@code True} if objects are equal.
     */
    @Override public boolean equals(Object obj) {
        A.notNull(key, "key");

        return obj != null && getClass() == obj.getClass() && key.equals(((AffinityKey)obj).key);
    }

    /** {@inheritDoc} */
    @Override public String toString() {
        return S.toString(AffinityKey.class, this);
    }
}
