/*
 * 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.jackrabbit.oak.segment;

import static org.apache.jackrabbit.guava.common.base.Preconditions.checkArgument;
import static java.util.Arrays.sort;
import static java.util.Collections.singleton;
import static org.apache.jackrabbit.oak.segment.MapRecord.SIZE_BITS;
import static org.apache.jackrabbit.oak.segment.RecordType.BLOB_ID;
import static org.apache.jackrabbit.oak.segment.RecordType.BLOCK;
import static org.apache.jackrabbit.oak.segment.RecordType.BRANCH;
import static org.apache.jackrabbit.oak.segment.RecordType.BUCKET;
import static org.apache.jackrabbit.oak.segment.RecordType.LEAF;
import static org.apache.jackrabbit.oak.segment.RecordType.LIST;
import static org.apache.jackrabbit.oak.segment.RecordType.NODE;
import static org.apache.jackrabbit.oak.segment.RecordType.TEMPLATE;
import static org.apache.jackrabbit.oak.segment.RecordType.VALUE;
import static org.apache.jackrabbit.oak.segment.Segment.RECORD_ID_BYTES;
import static org.apache.jackrabbit.oak.segment.Segment.SMALL_LIMIT;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

final class RecordWriters {
    private RecordWriters() {}

    /**
     * Base class for all record writers
     */
    public abstract static class RecordWriter {
        private final RecordType type;
        protected final int size;
        protected final Collection<RecordId> ids;

        protected RecordWriter(RecordType type, int size,
                Collection<RecordId> ids) {
            this.type = type;
            this.size = size;
            this.ids = ids;
        }

        protected RecordWriter(RecordType type, int size, RecordId id) {
            this(type, size, singleton(id));
        }

        protected RecordWriter(RecordType type, int size) {
            this(type, size, Collections.<RecordId> emptyList());
        }

        public final RecordId write(SegmentBufferWriter writer, SegmentStore store) throws IOException {
            RecordId id = writer.prepare(type, size, ids, store);
            return writeRecordContent(id, writer);
        }

        protected abstract RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer);
    }

    public static RecordWriter newMapLeafWriter(int level, Collection<MapEntry> entries) {
        return new MapLeafWriter(level, entries);
    }

    public static RecordWriter newMapLeafWriter() {
        return new MapLeafWriter();
    }

    public static RecordWriter newMapBranchWriter(int level, int entryCount, int bitmap, List<RecordId> ids) {
        return new MapBranchWriter(level, entryCount, bitmap, ids);
    }

    public static RecordWriter newMapBranchWriter(int bitmap, List<RecordId> ids) {
        return new MapBranchWriter(bitmap, ids);
    }

    public static RecordWriter newListWriter(int count, RecordId lid) {
        return new ListWriter(count, lid);
    }

    public static RecordWriter newListWriter() {
        return new ListWriter();
    }

    public static RecordWriter newListBucketWriter(List<RecordId> ids) {
        return new ListBucketWriter(ids);
    }

    public static RecordWriter newBlockWriter(byte[] bytes, int offset, int length) {
        return new BlockWriter(bytes, offset, length);
    }

    public static RecordWriter newValueWriter(RecordId rid, long len) {
        return new SingleValueWriter(rid, len);
    }

    public static RecordWriter newValueWriter(int length, byte[] data) {
        return new ArrayValueWriter(length, data);
    }

    /**
     * Write a large blob ID. A blob ID is considered large if the length of its
     * binary representation is equal to or greater than {@code
     * Segment.BLOB_ID_SMALL_LIMIT}.
     */
    public static RecordWriter newBlobIdWriter(RecordId rid) {
        return new LargeBlobIdWriter(rid);
    }

    /**
     * Write a small blob ID. A blob ID is considered small if the length of its
     * binary representation is less than {@code Segment.BLOB_ID_SMALL_LIMIT}.
     */
    public static RecordWriter newBlobIdWriter(byte[] blobId) {
        return new SmallBlobIdWriter(blobId);
    }

    public static RecordWriter newTemplateWriter(Collection<RecordId> ids,
            RecordId[] propertyNames, byte[] propertyTypes, int head, RecordId primaryId,
            List<RecordId> mixinIds, RecordId childNameId, RecordId propNamesId) {
        return new TemplateWriter(ids, propertyNames, propertyTypes, head, primaryId, mixinIds,
            childNameId, propNamesId);
    }

    public static RecordWriter newNodeStateWriter(RecordId stableId, List<RecordId> ids) {
        return new NodeStateWriter(stableId, ids);
    }

    /**
     * Map Leaf record writer.
     * @see RecordType#LEAF
     */
    private static class MapLeafWriter extends RecordWriter {
        private final int level;
        private final Collection<MapEntry> entries;

        private MapLeafWriter() {
            super(LEAF, 4);
            this.level = -1;
            this.entries = null;
        }

        private MapLeafWriter(int level, Collection<MapEntry> entries) {
            super(LEAF, 4 + entries.size() * 4, extractIds(entries));
            this.level = level;
            this.entries = entries;
        }

        private static List<RecordId> extractIds(Collection<MapEntry> entries) {
            List<RecordId> ids = new ArrayList<>(2 * entries.size());
            for (MapEntry entry : entries) {
                ids.add(entry.getKey());
                ids.add(entry.getValue());
            }
            return ids;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            if (entries != null) {
                int size = entries.size();
                writer.writeInt((level << SIZE_BITS) | size);

                // copy the entries to an array so we can sort them before
                // writing
                MapEntry[] array = entries.toArray(new MapEntry[size]);
                sort(array);

                for (MapEntry entry : array) {
                    writer.writeInt(entry.getHash());
                }
                for (MapEntry entry : array) {
                    writer.writeRecordId(entry.getKey());
                    writer.writeRecordId(entry.getValue());
                }
            } else {
                writer.writeInt(0);
            }
            return id;
        }
    }

    /**
     * Map Branch record writer.
     * @see RecordType#BRANCH
     */
    private static class MapBranchWriter extends RecordWriter {
        private final int level;
        private final int entryCount;
        private final int bitmap;

        /*
         * Write a regular map branch
         */
        private MapBranchWriter(int level, int entryCount, int bitmap, List<RecordId> ids) {
            super(BRANCH, 8, ids);
            this.level = level;
            this.entryCount = entryCount;
            this.bitmap = bitmap;
        }

        /*
         * Write a diff map
         */
        private MapBranchWriter(int bitmap, List<RecordId> ids) {
            // level = 0 and and entryCount = -1 -> this is a map diff
            this(0, -1, bitmap, ids);
        }

        @Override
        protected RecordId writeRecordContent(RecordId id, SegmentBufferWriter writer) {
            // -1 to encode a map diff (if level == 0 and entryCount == -1)
            writer.writeInt((level << SIZE_BITS) | entryCount);
            writer.writeInt(bitmap);
            for (RecordId mapId : ids) {
                writer.writeRecordId(mapId);
            }
            return id;
        }
    }

    /**
     * List record writer.
     * @see RecordType#LIST
     */
    private static class ListWriter extends RecordWriter  {
        private final int count;
        private final RecordId lid;

        private ListWriter() {
            super(LIST, 4);
            count = 0;
            lid = null;
        }

        private ListWriter(int count, RecordId lid) {
            super(LIST, 4, lid);
            this.count = count;
            this.lid = lid;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            writer.writeInt(count);
            if (lid != null) {
                writer.writeRecordId(lid);
            }
            return id;
        }
    }

    /**
     * List Bucket record writer.
     *
     * @see RecordType#BUCKET
     */
    private static class ListBucketWriter extends RecordWriter {

        private ListBucketWriter(List<RecordId> ids) {
            super(BUCKET, 0, ids);
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            for (RecordId bucketId : ids) {
                writer.writeRecordId(bucketId);
            }
            return id;
        }
    }

    /**
     * Block record writer.
     * @see RecordType#BLOCK
     */
    private static class BlockWriter extends RecordWriter {
        private final byte[] bytes;
        private final int offset;

        private BlockWriter(byte[] bytes, int offset, int length) {
            super(BLOCK, length);
            this.bytes = bytes;
            this.offset = offset;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            writer.writeBytes(bytes, offset, size);
            return id;
        }
    }

    /**
     * Single RecordId record writer.
     * @see RecordType#VALUE
     */
    private static class SingleValueWriter extends RecordWriter {
        private final RecordId rid;
        private final long len;

        private SingleValueWriter(RecordId rid, long len) {
            super(VALUE, 8, rid);
            this.rid = rid;
            this.len = len;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            writer.writeLong(len);
            writer.writeRecordId(rid);
            return id;
        }
    }

    /**
     * Bye array record writer. Used as a special case for short binaries (up to
     * about {@code Segment#MEDIUM_LIMIT}): store them directly as small or
     * medium-sized value records.
     * @see Segment#MEDIUM_LIMIT
     * @see RecordType#VALUE
     */
    private static class ArrayValueWriter extends RecordWriter {
        private final int length;
        private final byte[] data;

        private ArrayValueWriter(int length, byte[] data) {
            super(VALUE, length + getSizeDelta(length));
            this.length = length;
            this.data = data;
        }

        private static boolean isSmallSize(int length) {
            return length < SMALL_LIMIT;
        }

        private static int getSizeDelta(int length) {
            if (isSmallSize(length)) {
                return 1;
            } else {
                return 2;
            }
        }

        @Override
        protected RecordId writeRecordContent(RecordId id, SegmentBufferWriter writer) {
            if (isSmallSize(length)) {
                writer.writeByte((byte) length);
            } else {
                writer.writeShort((short) ((length - SMALL_LIMIT) | 0x8000));
            }
            writer.writeBytes(data, 0, length);
            return id;
        }
    }

    /**
     * Large Blob record writer. A blob ID is considered large if the length of
     * its binary representation is equal to or greater than
     * {@code Segment#BLOB_ID_SMALL_LIMIT}.
     *
     * @see Segment#BLOB_ID_SMALL_LIMIT
     * @see RecordType#BLOB_ID
     */
    private static class LargeBlobIdWriter extends RecordWriter {
        private final RecordId stringRecord;

        private LargeBlobIdWriter(RecordId stringRecord) {
            super(BLOB_ID, 1, stringRecord);
            this.stringRecord = stringRecord;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            // The length uses a fake "length" field that is always equal to
            // 0xF0.
            // This allows the code to take apart small from a large blob IDs.
            writer.writeByte((byte) 0xF0);
            writer.writeRecordId(stringRecord);
            return id;
        }
    }

    /**
     * Small Blob record writer. A blob ID is considered small if the length of
     * its binary representation is less than {@code Segment#BLOB_ID_SMALL_LIMIT}.

     * @see Segment#BLOB_ID_SMALL_LIMIT
     * @see RecordType#BLOB_ID
     */
    private static class SmallBlobIdWriter extends RecordWriter {
        private final byte[] blobId;

        private SmallBlobIdWriter(byte[] blobId) {
            super(BLOB_ID, 2 + blobId.length);
            checkArgument(blobId.length < Segment.BLOB_ID_SMALL_LIMIT);
            this.blobId = blobId;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            int length = blobId.length;
            writer.writeShort((short) (length | 0xE000));
            writer.writeBytes(blobId, 0, length);
            return id;
        }
    }

    /**
     * Template record writer.
     * @see RecordType#TEMPLATE
     */
    private static class TemplateWriter extends RecordWriter {
        private final RecordId[] propertyNames;
        private final byte[] propertyTypes;
        private final int head;
        private final RecordId primaryId;
        private final List<RecordId> mixinIds;
        private final RecordId childNameId;
        private final RecordId propNamesId;

        private TemplateWriter(Collection<RecordId> ids, RecordId[] propertyNames,
                byte[] propertyTypes, int head, RecordId primaryId, List<RecordId> mixinIds,
                RecordId childNameId, RecordId propNamesId) {
            super(TEMPLATE, 4 + propertyTypes.length, ids);
            this.propertyNames = propertyNames;
            this.propertyTypes = propertyTypes;
            this.head = head;
            this.primaryId = primaryId;
            this.mixinIds = mixinIds;
            this.childNameId = childNameId;
            this.propNamesId = propNamesId;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id,
                SegmentBufferWriter writer) {
            writer.writeInt(head);
            if (primaryId != null) {
                writer.writeRecordId(primaryId);
            }
            if (mixinIds != null) {
                for (RecordId mixinId : mixinIds) {
                    writer.writeRecordId(mixinId);
                }
            }
            if (childNameId != null) {
                writer.writeRecordId(childNameId);
            }
            if (propNamesId != null) {
                writer.writeRecordId(propNamesId);
            }
            for (int i = 0; i < propertyNames.length; i++) {
                writer.writeByte(propertyTypes[i]);
            }
            return id;
        }
    }

    /**
     * Node State record writer.
     * @see RecordType#NODE
     */
    private static class NodeStateWriter extends RecordWriter {
        private final RecordId stableId;

        private NodeStateWriter(RecordId stableId, List<RecordId> ids) {
            super(NODE, RECORD_ID_BYTES, ids);
            this.stableId = stableId;
        }

        @Override
        protected RecordId writeRecordContent(RecordId id, SegmentBufferWriter writer) {

            // Write the stable record ID. If no stable ID exists (in case of a
            // new node state), it is generated from the current record ID. In
            // this case, the generated stable ID is only a marker and is not a
            // reference to another record.

            if (stableId == null) {
                // Write this node's record id to indicate that the stable id is not
                // explicitly stored.
                writer.writeRecordId(id);
            } else {
                writer.writeRecordId(stableId);
            }

            for (RecordId recordId : ids) {
                writer.writeRecordId(recordId);
            }
            return id;
        }
    }

}
