/*
 * 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.persistence.db;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheRebalanceMode;
import org.apache.ignite.cache.CacheWriteSynchronizationMode;
import org.apache.ignite.cache.affinity.Affinity;
import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
import org.apache.ignite.cluster.ClusterNode;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.DataRegionConfiguration;
import org.apache.ignite.configuration.DataStorageConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.configuration.WALMode;
import org.apache.ignite.events.CacheRebalancingEvent;
import org.apache.ignite.events.Event;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.IgniteInternalFuture;
import org.apache.ignite.internal.processors.cache.persistence.GridCacheDatabaseSharedManager;
import org.apache.ignite.internal.util.GridLongList;
import org.apache.ignite.internal.util.lang.GridPlainRunnable;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.WithSystemProperty;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;

import static org.apache.ignite.events.EventType.EVT_CACHE_REBALANCE_PART_UNLOADED;
import static org.apache.ignite.events.EventType.EVT_PAGE_REPLACEMENT_STARTED;

/**
 *
 */
public class IgnitePdsPageReplacementDuringPartitionClearTest extends GridCommonAbstractTest {
    /** */
    private static final String CACHE_NAME = "cache";

    /** Number of partitions in the test. */
    private static final int PARTS = 128;

    /** {@inheritDoc} */
    @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception {
        IgniteConfiguration cfg = super.getConfiguration(gridName);

        CacheConfiguration ccfg = new CacheConfiguration(CACHE_NAME)
            .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)
            .setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC)
            .setAffinity(new RendezvousAffinityFunction(false, PARTS))
            .setRebalanceMode(CacheRebalanceMode.SYNC)
            .setBackups(1);

        cfg.setCacheConfiguration(ccfg);

        // Intentionally set small page cache size.
        DataStorageConfiguration memCfg = new DataStorageConfiguration()
            .setDefaultDataRegionConfiguration(
                new DataRegionConfiguration().setMaxSize(50L * 1024 * 1024).setPersistenceEnabled(true))
            .setWalMode(WALMode.LOG_ONLY);

        cfg.setDataStorageConfiguration(memCfg);

        cfg.setIncludeEventTypes(EVT_CACHE_REBALANCE_PART_UNLOADED, EVT_PAGE_REPLACEMENT_STARTED);

        return cfg;
    }

    /** {@inheritDoc} */
    @Override protected long getTestTimeout() {
        return 20 * 60 * 1000;
    }

    /**
     * @throws Exception if failed.
     */
    @Test
    @WithSystemProperty(key = GridCacheDatabaseSharedManager.IGNITE_PDS_CHECKPOINT_TEST_SKIP_SYNC, value = "true")
    public void testPageEvictionOnNodeStart() throws Exception {
        cleanPersistenceDir();

        startGrids(2);

        AtomicBoolean stop = new AtomicBoolean(false);

        try {
            Ignite ig = ignite(0);

            ig.cluster().active(true);

            ig.cluster().baselineAutoAdjustEnabled(false);

            int last = loadDataUntilPageReplacement(ignite(0), ignite(1));

            IgniteInternalFuture<?> fut = loadAsync(ig, stop, last);

            EvictionListener evictLsnr = new EvictionListener();

            ignite(0).events().localListen(evictLsnr, EVT_CACHE_REBALANCE_PART_UNLOADED);
            ignite(1).events().localListen(evictLsnr, EVT_CACHE_REBALANCE_PART_UNLOADED);

            IgniteEx igNew = startGrid(2);

            info(">>>>>>>>>>>");
            info(">>>>>>>>>>>");
            info(">>>>>>>>>>>");

            igNew.cluster().setBaselineTopology(3);

            awaitPartitionMapExchange();

            Map<ClusterNode, GridLongList> affinityAfter = allPartitions(igNew);

            evictLsnr.waitPartitionsEvicted(igNew.cluster().localNode(), affinityAfter);

            stop.set(true);

            fut.get();
        }
        finally {
            stop.set(true);

            stopAllGrids();

            cleanPersistenceDir();
        }
    }

    /**
     * @param ig1 Ignite to load.
     * @param ig2 Ignite to load.
     * @return Start index for next updates.
     * @throws IgniteCheckedException If failed.
     */
    private int loadDataUntilPageReplacement(Ignite ig1, Ignite ig2) throws IgniteCheckedException {
        AtomicInteger idx = new AtomicInteger();

        CoundDownFilter lsnr = new CoundDownFilter(EVT_PAGE_REPLACEMENT_STARTED, 2);

        ig1.events().localListen(lsnr, EVT_PAGE_REPLACEMENT_STARTED);
        ig2.events().localListen(lsnr, EVT_PAGE_REPLACEMENT_STARTED);

        IgniteInternalFuture<Long> fut = GridTestUtils.runMultiThreadedAsync(new GridPlainRunnable() {
            @Override public void run() {
                IgniteCache<Object, Object> cache = ig1.cache(CACHE_NAME);

                while (!lsnr.isReady()) {
                    int start = idx.getAndAdd(100);

                    Map<Integer, TestValue> putMap = new HashMap<>(100, 1.f);

                    for (int i = 0; i < 100; i++)
                        putMap.put(start + i, new TestValue(start + i));

                    cache.putAll(putMap);
                }
            }
        }, Runtime.getRuntime().availableProcessors(), "initial-load-runner");

        fut.get();

        return idx.get();
    }

    /**
     * Calculates mapping from nodes to partitions.
     *
     * @param ig Ignite instance.
     * @return Map.
     */
    private Map<ClusterNode, GridLongList> allPartitions(Ignite ig) {
        Map<ClusterNode, GridLongList> res = new HashMap<>(ig.cluster().nodes().size(), 1.f);

        Affinity<Object> aff = ig.affinity(CACHE_NAME);

        for (int i = 0; i < PARTS; i++) {
            Collection<ClusterNode> nodes = aff.mapPartitionToPrimaryAndBackups(i);

            for (ClusterNode node : nodes)
                res.computeIfAbsent(node, k -> new GridLongList(2)).add(i);
        }

        return res;
    }

    /**
     * @param ig Ignite instance.
     * @param stopFlag Stop flag.
     * @param start Load key start index.
     * @return Completion future.
     */
    private IgniteInternalFuture<?> loadAsync(Ignite ig, AtomicBoolean stopFlag, int start) {
        AtomicInteger generator = new AtomicInteger(start);

        return GridTestUtils.runMultiThreadedAsync(new GridPlainRunnable() {
            @Override public void run() {
                IgniteCache<Integer, TestValue> cache = ig.cache(CACHE_NAME);

                while (!stopFlag.get()) {
                    int idx = generator.getAndAdd(100);

                    Map<Integer, TestValue> putMap = new HashMap<>(100, 1.f);

                    for (int i = 0; i < 100; i++)
                        putMap.put(idx + i, new TestValue(idx + i));

                    cache.putAll(putMap);
                }
            }
        }, Runtime.getRuntime().availableProcessors(), "load-runner");
    }

    /**
     *
     */
    private static class CoundDownFilter implements IgnitePredicate<Event> {
        /** */
        private final int evtType;

        /** */
        private final AtomicInteger cnt;

        /**
         * @param evtType Event type.
         */
        private CoundDownFilter(int evtType, int cnt) {
            this.evtType = evtType;

            this.cnt = new AtomicInteger(cnt);
        }

        /** {@inheritDoc} */
        @Override public boolean apply(Event evt) {
            if (evt.type() == evtType)
                cnt.decrementAndGet();

            return cnt.get() > 0;
        }

        /**
         * @return Await result.
         */
        public boolean isReady() {
            return cnt.get() <= 0;
        }
    }

    /**
     *
     */
    private static class EvictionListener implements IgnitePredicate<Event> {
        /** */
        private final GridLongList unloadedParts = new GridLongList();

        /** {@inheritDoc} */
        @Override public boolean apply(Event evt) {
            if (evt.type() == EVT_CACHE_REBALANCE_PART_UNLOADED) {
                CacheRebalancingEvent rebEvt = (CacheRebalancingEvent)evt;

                synchronized (this) {
                    unloadedParts.add(rebEvt.partition());

                    unloadedParts.sort();

                    notifyAll();
                }
            }

            return true;
        }

        /**
         * @param node Node to wait.
         * @param affinityAfter Affinity after rebalancing.
         * @throws InterruptedException If calling thread is interrupted.
         */
        public void waitPartitionsEvicted(
            ClusterNode node,
            Map<ClusterNode, GridLongList> affinityAfter
        ) throws InterruptedException {
            GridLongList movedParts = affinityAfter.get(node);

            synchronized (this) {
                while (!unloadedParts.equals(movedParts))
                    wait();
            }
        }
    }

    /**
     *
     */
    private static class TestValue {
        /** */
        private int id;

        /** */
        private final byte[] payload = new byte[512];

        /**
         * @param id ID.
         */
        private TestValue(int id) {
            this.id = id;
        }

        /**
         * @return ID.
         */
        public int getId() {
            return id;
        }

        /**
         * @return Payload.
         */
        public boolean hasPayload() {
            return payload != null;
        }
    }
}