/*
 * 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.cache.query.index.sorted.inline;

import java.util.List;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.IgniteSystemProperties;
import org.apache.ignite.failure.FailureType;
import org.apache.ignite.internal.cache.query.index.SortOrder;
import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyDefinition;
import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypeSettings;
import org.apache.ignite.internal.cache.query.index.sorted.IndexKeyTypes;
import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
import org.apache.ignite.internal.cache.query.index.sorted.IndexRowCache;
import org.apache.ignite.internal.cache.query.index.sorted.IndexRowImpl;
import org.apache.ignite.internal.cache.query.index.sorted.InlineIndexRowHandler;
import org.apache.ignite.internal.cache.query.index.sorted.InlineIndexRowHandlerFactory;
import org.apache.ignite.internal.cache.query.index.sorted.MetaPageInfo;
import org.apache.ignite.internal.cache.query.index.sorted.SortedIndexDefinition;
import org.apache.ignite.internal.cache.query.index.sorted.ThreadLocalRowHandlerHolder;
import org.apache.ignite.internal.cache.query.index.sorted.inline.io.AbstractInlineInnerIO;
import org.apache.ignite.internal.cache.query.index.sorted.inline.io.AbstractInlineLeafIO;
import org.apache.ignite.internal.cache.query.index.sorted.inline.io.MvccIO;
import org.apache.ignite.internal.metric.IoStatisticsHolder;
import org.apache.ignite.internal.pagemem.FullPageId;
import org.apache.ignite.internal.pagemem.PageIdAllocator;
import org.apache.ignite.internal.pagemem.PageIdUtils;
import org.apache.ignite.internal.pagemem.PageMemory;
import org.apache.ignite.internal.pagemem.wal.record.PageSnapshot;
import org.apache.ignite.internal.processors.cache.GridCacheContext;
import org.apache.ignite.internal.processors.cache.IgniteCacheOffheapManager;
import org.apache.ignite.internal.processors.cache.mvcc.MvccUtils;
import org.apache.ignite.internal.processors.cache.persistence.CacheDataRowAdapter;
import org.apache.ignite.internal.processors.cache.persistence.tree.BPlusTree;
import org.apache.ignite.internal.processors.cache.persistence.tree.CorruptedTreeException;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.BPlusMetaIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIoResolver;
import org.apache.ignite.internal.processors.cache.persistence.tree.reuse.ReuseList;
import org.apache.ignite.internal.processors.cache.tree.mvcc.data.MvccDataRow;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;

import static org.apache.ignite.internal.cache.query.index.sorted.inline.types.NullableInlineIndexKeyType.CANT_BE_COMPARE;
import static org.apache.ignite.internal.cache.query.index.sorted.inline.types.NullableInlineIndexKeyType.COMPARE_UNSUPPORTED;

/**
 * BPlusTree where nodes stores inlined index keys.
 */
public class InlineIndexTree extends BPlusTree<IndexRow, IndexRow> {
    /** Amount of bytes to store inlined index keys. */
    private final int inlineSize;

    /** Index key type settings for this tree. */
    private final IndexKeyTypeSettings keyTypeSettings;

    /** Recommends change inline size if needed. */
    private final InlineRecommender recommender;

    /** Whether tree is created from scratch or reused from underlying store. */
    private final boolean created;

    /** Definition of index. */
    private final SortedIndexDefinition def;

    /** */
    private final InlineIndexRowHandler rowHnd;

    /** Cache context. */
    private final GridCacheContext<?, ?> cctx;

    /** Statistics holder used by underlying BPlusTree. */
    private final IoStatisticsHolder stats;

    /** */
    private final IgniteLogger log;

    /** Row cache. */
    private final @Nullable IndexRowCache idxRowCache;

    /** Whether MVCC is enabled. */
    private final boolean mvccEnabled;

    /**
     * Constructor.
     */
    public InlineIndexTree(
        SortedIndexDefinition def,
        GridCacheContext<?, ?> cctx,
        String treeName,
        IgniteCacheOffheapManager offheap,
        ReuseList reuseList,
        PageMemory pageMemory,
        PageIoResolver pageIoResolver,
        long metaPageId,
        boolean initNew,
        int configuredInlineSize,
        IndexKeyTypeSettings keyTypeSettings,
        @Nullable IndexRowCache idxRowCache,
        @Nullable IoStatisticsHolder stats,
        InlineIndexRowHandlerFactory rowHndFactory,
        InlineRecommender recommender) throws IgniteCheckedException {
        super(
            treeName,
            cctx.groupId(),
            cctx.group().name(),
            pageMemory,
            cctx.shared().wal(),
            offheap.globalRemoveId(),
            metaPageId,
            reuseList,
            PageIdAllocator.FLAG_IDX,
            cctx.shared().kernalContext().failure(),
            null,
            pageIoResolver
        );

        this.cctx = cctx;

        log = cctx.kernalContext().config().getGridLogger();

        this.stats = stats;

        created = initNew;

        this.def = def;

        this.idxRowCache = idxRowCache;

        mvccEnabled = cctx.mvccEnabled();

        if (!initNew) {
            // Init from metastore.
            // Page is ready - read meta information.
            MetaPageInfo metaInfo = metaInfo();

            if (def != null)
                def.initByMeta(initNew, metaInfo);

            inlineSize = metaInfo.inlineSize();
            setIos(inlineSize, mvccEnabled);

            boolean inlineObjSupported = inlineObjectSupported(def, metaInfo, rowHndFactory);

            keyTypeSettings
                .inlineObjHash(metaInfo.inlineObjectHash())
                .inlineObjSupported(inlineObjSupported);

            rowHnd = rowHndFactory.create(def, keyTypeSettings);

            if (!metaInfo.flagsSupported())
                upgradeMetaPage(inlineObjSupported);

        } else {
            def.initByMeta(initNew, null);

            rowHnd = rowHndFactory.create(def, keyTypeSettings);

            inlineSize = computeInlineSize(
                rowHnd.inlineIndexKeyTypes(), configuredInlineSize, cctx.config().getSqlIndexMaxInlineSize());

            setIos(inlineSize, mvccEnabled);
        }

        this.keyTypeSettings = keyTypeSettings;

        initTree(initNew, inlineSize);

        this.recommender = recommender;
    }

    /** */
    private void setIos(int inlineSize, boolean mvccEnabled) {
        setIos(
            AbstractInlineInnerIO.versions(inlineSize, mvccEnabled),
            AbstractInlineLeafIO.versions(inlineSize, mvccEnabled)
        );
    }

    /**
     * Find whether tree supports inlining objects or not.
     *
     * @param def Index definition.
     * @param metaInfo Metapage info.
     * @return {@code true} if inline object is supported by exists tree.
     */
    private boolean inlineObjectSupported(SortedIndexDefinition def, MetaPageInfo metaInfo,
        InlineIndexRowHandlerFactory rowHndFactory) {

        if (metaInfo.flagsSupported())
            return metaInfo.inlineObjectSupported();
        else {
            try {
                if (InlineObjectBytesDetector.objectMayBeInlined(metaInfo.inlineSize(), def.indexKeyDefinitions())) {
                    try {
                        InlineObjectBytesDetector inlineObjDetector = new InlineObjectBytesDetector(
                            metaInfo.inlineSize(), def.indexKeyDefinitions(), def.idxName(), log);

                        // Create a settings for case where java objects inilned as byte array.
                        IndexKeyTypeSettings keyTypeSettings = new IndexKeyTypeSettings()
                            .inlineObjSupported(true)
                            .inlineObjHash(false);

                        InlineIndexRowHandler rowHnd = rowHndFactory.create(def, keyTypeSettings);

                        ThreadLocalRowHandlerHolder.rowHandler(rowHnd);

                        findFirst(inlineObjDetector);

                        return inlineObjDetector.inlineObjectSupported();

                    } finally {
                        ThreadLocalRowHandlerHolder.clearRowHandler();
                    }
                }
                else
                    return false;
            }
            catch (IgniteCheckedException e) {
                throw new IgniteException("Unexpected exception on detect inline object", e);
            }
        }
    }

    /** {@inheritDoc} */
    @Override protected int compare(BPlusIO<IndexRow> io, long pageAddr, int idx, IndexRow row)
        throws IgniteCheckedException {

        if (inlineSize == 0) {
            IndexRow currRow = getRow(io, pageAddr, idx);

            int cmp = compareFullRows(currRow, row, 0);

            return cmp == 0 ? mvccCompare(currRow, row) : cmp;
        }

        int fieldOff = 0;

        // Use it when can't compare values (variable length, for example).
        int keyIdx;

        IndexRow currRow = null;

        int off = io.offset(idx);

        List<IndexKeyDefinition> keyDefs = def.indexKeyDefinitions();

        List<InlineIndexKeyType> keyTypes = rowHandler().inlineIndexKeyTypes();

        for (keyIdx = 0; keyIdx < keyTypes.size(); keyIdx++) {
            try {
                // If a search key is null then skip other keys (consider that null shows that we should get all
                // possible keys for that comparison).
                if (row.key(keyIdx) == null)
                    return 0;

                // Other keys are not inlined. Should compare as rows.
                if (keyIdx >= keyTypes.size())
                    break;

                int maxSize = inlineSize - fieldOff;

                InlineIndexKeyType keyType = keyTypes.get(keyIdx);

                IndexKeyDefinition keyDef = keyDefs.get(keyIdx);

                int cmp = COMPARE_UNSUPPORTED;

                // Value can be set up by user in query with different data type.
                // By default do not compare different types.
                if (keyDef.validate(row.key(keyIdx))) {
                    if (keyType.type() != IndexKeyTypes.JAVA_OBJECT || keyTypeSettings.inlineObjSupported()) {
                        cmp = keyType.compare(pageAddr, off + fieldOff, maxSize, row.key(keyIdx));

                        fieldOff += keyType.inlineSize(pageAddr, off + fieldOff);
                    }
                    // If inlining of POJO is not supported then fallback to previous logic.
                    else
                        break;
                }

                // Can't compare as inlined bytes are not enough for comparation.
                if (cmp == CANT_BE_COMPARE)
                    break;

                // Try compare stored values for inlined keys with different approach?
                if (cmp == COMPARE_UNSUPPORTED)
                    cmp = def.rowComparator().compareKey(
                        pageAddr, off + fieldOff, maxSize, row.key(keyIdx), keyType.type());

                if (cmp == CANT_BE_COMPARE || cmp == COMPARE_UNSUPPORTED)
                    break;

                if (cmp != 0)
                    return applySortOrder(cmp, keyDef.order().sortOrder());

            } catch (Exception e) {
                throw new IgniteException("Failed to store new index row.", e);
            }
        }

        if (keyIdx < keyDefs.size()) {
            recommender.recommend(row, inlineSize);

            if (currRow == null)
                currRow = getRow(io, pageAddr, idx);

            int ret = compareFullRows(currRow, row, keyIdx);

            if (ret != 0)
                return ret;
        }

        return mvccCompare((MvccIO) io, pageAddr, idx, row);
    }

    /** */
    private int compareFullRows(IndexRow currRow, IndexRow row, int from) throws IgniteCheckedException {
        if (currRow == row)
            return 0;

        for (int i = from; i < rowHandler().indexKeyDefinitions().size(); i++) {
            // If a search key is null then skip other keys (consider that null shows that we should get all
            // possible keys for that comparison).
            if (row.key(i) == null)
                return 0;

            int c = def.rowComparator().compareKey(currRow, row, i);

            if (c != 0)
                return applySortOrder(Integer.signum(c), def.indexKeyDefinitions().get(i).order().sortOrder());
        }

        return 0;
    }

    /**
     * Perform sort order correction.
     *
     * @param c Compare result.
     * @param order Sort order.
     * @return Fixed compare result.
     */
    private static int applySortOrder(int c, SortOrder order) {
        return order == SortOrder.ASC ? c : -c;
    }

    /** Creates an index row for this tree. */
    public IndexRowImpl createIndexRow(long link) throws IgniteCheckedException {
        IndexRowImpl cachedRow = idxRowCache == null ? null : idxRowCache.get(link);

        if (cachedRow != null)
            return cachedRow;

        CacheDataRowAdapter row = new CacheDataRowAdapter(link);

        row.initFromLink(cacheContext().group(), CacheDataRowAdapter.RowData.FULL, true);

        IndexRowImpl r = new IndexRowImpl(rowHandler(), row);

        if (idxRowCache != null)
            idxRowCache.put(r);

        return r;
    }

    /** Creates an mvcc index row for this tree. */
    public IndexRowImpl createMvccIndexRow(long link, long mvccCrdVer, long mvccCntr, int mvccOpCntr) throws IgniteCheckedException {
        IndexRowImpl cachedRow = idxRowCache == null ? null : idxRowCache.get(link);

        if (cachedRow != null)
            return cachedRow;

        int partId = PageIdUtils.partId(PageIdUtils.pageId(link));

        MvccDataRow row = new MvccDataRow(
            cacheContext().group(),
            0,
            link,
            partId,
            null,
            mvccCrdVer,
            mvccCntr,
            mvccOpCntr,
            true
        );

        IndexRowImpl r = new IndexRowImpl(rowHandler(), row);

        if (idxRowCache != null)
            idxRowCache.put(r);

        return r;
    }

    /** {@inheritDoc} */
    @Override public IndexRow getRow(BPlusIO<IndexRow> io, long pageAddr, int idx, Object ignore)
        throws IgniteCheckedException {

        return io.getLookupRow(this, pageAddr, idx);
    }

    /** */
    public int inlineSize() {
        return inlineSize;
    }

    /**
     * @param keyTypes Index key types.
     * @param cfgInlineSize Inline size from index config.
     * @param maxInlineSize Max inline size from cache config.
     * @return Inline size.
     */
    public static int computeInlineSize(
        List<InlineIndexKeyType> keyTypes,
        int cfgInlineSize,
        int maxInlineSize
    ) {
        if (cfgInlineSize == 0)
            return 0;

        if (F.isEmpty(keyTypes))
            return 0;

        if (cfgInlineSize != -1)
            return Math.min(PageIO.MAX_PAYLOAD_SIZE, cfgInlineSize);

        int propSize = maxInlineSize == -1
            ? IgniteSystemProperties.getInteger(IgniteSystemProperties.IGNITE_MAX_INDEX_PAYLOAD_SIZE, IGNITE_MAX_INDEX_PAYLOAD_SIZE_DEFAULT)
            : maxInlineSize;

        int size = 0;

        for (InlineIndexKeyType keyType: keyTypes) {
            if (keyType.inlineSize() <= 0) {
                size = propSize;
                break;
            }

            size += keyType.inlineSize();
        }

        return Math.min(PageIO.MAX_PAYLOAD_SIZE, size);
    }

    /** */
    public GridCacheContext<?, ?> cacheContext() {
        return cctx;
    }

    /** Default value for {@code IGNITE_MAX_INDEX_PAYLOAD_SIZE} */
    public static final int IGNITE_MAX_INDEX_PAYLOAD_SIZE_DEFAULT = 10;

    /**
     * @return Inline size.
     * @throws IgniteCheckedException If failed.
     */
    public MetaPageInfo metaInfo() throws IgniteCheckedException {
        final long metaPage = acquirePage(metaPageId);

        try {
            long pageAddr = readLock(metaPageId, metaPage); // Meta can't be removed.

            assert pageAddr != 0 : "Failed to read lock meta page [metaPageId=" +
                U.hexLong(metaPageId) + ']';

            try {
                BPlusMetaIO io = BPlusMetaIO.VERSIONS.forPage(pageAddr);

                return new MetaPageInfo(io, pageAddr);
            }
            finally {
                readUnlock(metaPageId, metaPage, pageAddr);
            }
        }
        finally {
            releasePage(metaPageId, metaPage);
        }
    }

    /**
     * Update root meta page if need (previous version not supported features flags
     * and created product version on root meta page).
     *
     * @param inlineObjSupported inline POJO by created tree flag.
     * @throws IgniteCheckedException On error.
     */
    private void upgradeMetaPage(boolean inlineObjSupported) throws IgniteCheckedException {
        final long metaPage = acquirePage(metaPageId);

        try {
            long pageAddr = writeLock(metaPageId, metaPage); // Meta can't be removed.

            assert pageAddr != 0 : "Failed to read lock meta page [metaPageId=" +
                U.hexLong(metaPageId) + ']';

            try {
                BPlusMetaIO.upgradePageVersion(pageAddr, inlineObjSupported, false, pageSize());

                if (wal != null)
                    wal.log(new PageSnapshot(new FullPageId(metaPageId, grpId),
                        pageAddr, pageMem.pageSize(), pageMem.realPageSize(grpId)));
            }
            finally {
                writeUnlock(metaPageId, metaPage, pageAddr, true);
            }
        }
        finally {
            releasePage(metaPageId, metaPage);
        }
    }

    /**
     * Copy info from another meta page.
     * @param info Meta page info.
     * @throws IgniteCheckedException If failed.
     */
    public void copyMetaInfo(MetaPageInfo info) throws IgniteCheckedException {
        final long metaPage = acquirePage(metaPageId);

        try {
            long pageAddr = writeLock(metaPageId, metaPage); // Meta can't be removed.

            assert pageAddr != 0 : "Failed to read lock meta page [metaPageId=" +
                U.hexLong(metaPageId) + ']';

            try {
                BPlusMetaIO.setValues(
                    pageAddr,
                    info.inlineSize(),
                    info.useUnwrappedPk(),
                    info.inlineObjectSupported(),
                    info.inlineObjectHash()
                );
            }
            finally {
                writeUnlock(metaPageId, metaPage, pageAddr, true);
            }
        }
        finally {
            releasePage(metaPageId, metaPage);
        }
    }

    /** */
    public boolean created() {
        return created;
    }

    /**
     * Construct the exception and invoke failure processor.
     *
     * @param msg Message.
     * @param cause Cause.
     * @param grpId Group id.
     * @param pageIds Pages ids.
     * @return New CorruptedTreeException instance.
     */
    @Override protected CorruptedTreeException corruptedTreeException(String msg, Throwable cause, int grpId, long... pageIds) {
        CorruptedTreeException e = new CorruptedTreeException(msg, cause, grpId, grpName, def.idxName().cacheName(),
            def.idxName().idxName(), pageIds);

        processFailure(FailureType.CRITICAL_ERROR, e);

        return e;
    }

    /** {@inheritDoc} */
    @Override protected void temporaryReleaseLock() {
        cctx.kernalContext().cache().context().database().checkpointReadUnlock();
        cctx.kernalContext().cache().context().database().checkpointReadLock();
    }

    /** {@inheritDoc} */
    @Override protected long maxLockHoldTime() {
        long sysWorkerBlockedTimeout = cctx.kernalContext().workersRegistry().getSystemWorkerBlockedTimeout();

        // Using timeout value reduced by 10 times to increase possibility of lock releasing before timeout.
        return sysWorkerBlockedTimeout == 0 ? Long.MAX_VALUE : (sysWorkerBlockedTimeout / 10);
    }

    /** {@inheritDoc} */
    @Override protected IoStatisticsHolder statisticsHolder() {
        return stats != null ? stats : super.statisticsHolder();
    }

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

    /**
     * @return Index row handler for this tree. Row handler for a tree can be set externally with the holder.
     */
    public InlineIndexRowHandler rowHandler() {
        return rowHnd != null ? rowHnd : ThreadLocalRowHandlerHolder.rowHandler();
    }

    /**
     * @param io IO.
     * @param pageAddr Page address.
     * @param idx Item index.
     * @param row Search row.
     * @return Comparison result.
     */
    private int mvccCompare(MvccIO io, long pageAddr, int idx, IndexRow row) {
        if (!mvccEnabled || row.indexSearchRow())
            return 0;

        long crd = io.mvccCoordinatorVersion(pageAddr, idx);
        long cntr = io.mvccCounter(pageAddr, idx);
        int opCntr = io.mvccOperationCounter(pageAddr, idx);

        assert MvccUtils.mvccVersionIsValid(crd, cntr, opCntr);

        return -MvccUtils.compare(crd, cntr, opCntr, row);  // descending order
    }

    /**
     * @param r1 First row.
     * @param r2 Second row.
     * @return Comparison result.
     */
    private int mvccCompare(IndexRow r1, IndexRow r2) {
        if (!mvccEnabled || r2.indexSearchRow() || r1 == r2)
            return 0;

        long crdVer1 = r1.mvccCoordinatorVersion();
        long crdVer2 = r2.mvccCoordinatorVersion();

        int c = -Long.compare(crdVer1, crdVer2);

        if (c != 0)
            return c;

        return -Long.compare(r1.mvccCounter(), r2.mvccCounter());
    }
}
