/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.repositories.blobstore;

import org.apache.lucene.codecs.CodecUtil;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexFormatTooNewException;
import org.apache.lucene.index.IndexFormatTooOldException;
import org.apache.lucene.store.OutputStreamIndexOutput;
import org.elasticsearch.common.CheckedFunction;
import org.elasticsearch.common.blobstore.BlobContainer;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.compress.CompressorFactory;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.lucene.store.ByteArrayIndexInput;
import org.elasticsearch.common.lucene.store.IndexOutputOutputStream;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.gateway.CorruptStateException;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;

/**
 * Snapshot metadata file format used in v2.0 and above
 */
public class ChecksumBlobStoreFormat<T extends ToXContent> extends BlobStoreFormat<T> {

    private static final String TEMP_FILE_PREFIX = "pending-";

    private static final XContentType DEFAULT_X_CONTENT_TYPE = XContentType.SMILE;

    // The format version
    public static final int VERSION = 1;

    private static final int BUFFER_SIZE = 4096;

    protected final XContentType xContentType;

    protected final boolean compress;

    private final String codec;

    /**
     * @param codec          codec name
     * @param blobNameFormat format of the blobname in {@link String#format} format
     * @param reader         prototype object that can deserialize T from XContent
     * @param compress       true if the content should be compressed
     * @param xContentType   content type that should be used for write operations
     */
    public ChecksumBlobStoreFormat(String codec, String blobNameFormat, CheckedFunction<XContentParser, T, IOException> reader,
                                   NamedXContentRegistry namedXContentRegistry, boolean compress, XContentType xContentType) {
        super(blobNameFormat, reader, namedXContentRegistry);
        this.xContentType = xContentType;
        this.compress = compress;
        this.codec = codec;
    }

    /**
     * @param codec          codec name
     * @param blobNameFormat format of the blobname in {@link String#format} format
     * @param reader         prototype object that can deserialize T from XContent
     * @param compress       true if the content should be compressed
     */
    public ChecksumBlobStoreFormat(String codec, String blobNameFormat, CheckedFunction<XContentParser, T, IOException> reader,
                                   NamedXContentRegistry namedXContentRegistry, boolean compress) {
        this(codec, blobNameFormat, reader, namedXContentRegistry, compress, DEFAULT_X_CONTENT_TYPE);
    }

    /**
     * Reads blob with specified name without resolving the blobName using using {@link #blobName} method.
     *
     * @param blobContainer blob container
     * @param blobName blob name
     */
    public T readBlob(BlobContainer blobContainer, String blobName) throws IOException {
        try (InputStream inputStream = blobContainer.readBlob(blobName)) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Streams.copy(inputStream, out);
            final byte[] bytes = out.toByteArray();
            final String resourceDesc = "ChecksumBlobStoreFormat.readBlob(blob=\"" + blobName + "\")";
            try (ByteArrayIndexInput indexInput = new ByteArrayIndexInput(resourceDesc, bytes)) {
                CodecUtil.checksumEntireFile(indexInput);
                CodecUtil.checkHeader(indexInput, codec, VERSION, VERSION);
                long filePointer = indexInput.getFilePointer();
                long contentSize = indexInput.length() - CodecUtil.footerLength() - filePointer;
                BytesReference bytesReference = new BytesArray(bytes, (int) filePointer, (int) contentSize);
                return read(bytesReference);
            } catch (CorruptIndexException | IndexFormatTooOldException | IndexFormatTooNewException ex) {
                // we trick this into a dedicated exception with the original stacktrace
                throw new CorruptStateException(ex);
            }
        }
    }

    /**
     * Writes blob in atomic manner with resolving the blob name using {@link #blobName} and {@link #tempBlobName} methods.
     * <p>
     * The blob will be compressed and checksum will be written if required.
     *
     * Atomic move might be very inefficient on some repositories. It also cannot override existing files.
     *
     * @param obj           object to be serialized
     * @param blobContainer blob container
     * @param name          blob name
     */
    public void writeAtomic(T obj, BlobContainer blobContainer, String name) throws IOException {
        String blobName = blobName(name);
        String tempBlobName = tempBlobName(name);
        writeBlob(obj, blobContainer, tempBlobName);
        try {
            blobContainer.move(tempBlobName, blobName);
        } catch (IOException ex) {
            // Move failed - try cleaning up
            blobContainer.deleteBlob(tempBlobName);
            throw ex;
        }
    }

    /**
     * Writes blob with resolving the blob name using {@link #blobName} method.
     * <p>
     * The blob will be compressed and checksum will be written if required.
     *
     * @param obj           object to be serialized
     * @param blobContainer blob container
     * @param name          blob name
     */
    public void write(T obj, BlobContainer blobContainer, String name) throws IOException {
        String blobName = blobName(name);
        writeBlob(obj, blobContainer, blobName);
    }

    /**
     * Writes blob in atomic manner without resolving the blobName using using {@link #blobName} method.
     * <p>
     * The blob will be compressed and checksum will be written if required.
     *
     * @param obj           object to be serialized
     * @param blobContainer blob container
     * @param blobName          blob name
     */
    protected void writeBlob(T obj, BlobContainer blobContainer, String blobName) throws IOException {
        BytesReference bytes = write(obj);
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
            final String resourceDesc = "ChecksumBlobStoreFormat.writeBlob(blob=\"" + blobName + "\")";
            try (OutputStreamIndexOutput indexOutput = new OutputStreamIndexOutput(resourceDesc, blobName, byteArrayOutputStream, BUFFER_SIZE)) {
                CodecUtil.writeHeader(indexOutput, codec, VERSION);
                try (OutputStream indexOutputOutputStream = new IndexOutputOutputStream(indexOutput) {
                    @Override
                    public void close() throws IOException {
                        // this is important since some of the XContentBuilders write bytes on close.
                        // in order to write the footer we need to prevent closing the actual index input.
                    } }) {
                    bytes.writeTo(indexOutputOutputStream);
                }
                CodecUtil.writeFooter(indexOutput);
            }
            BytesArray bytesArray = new BytesArray(byteArrayOutputStream.toByteArray());
            try (InputStream stream = bytesArray.streamInput()) {
                blobContainer.writeBlob(blobName, stream, bytesArray.length());
            }
        }
    }

    /**
     * Returns true if the blob is a leftover temporary blob.
     *
     * The temporary blobs might be left after failed atomic write operation.
     */
    public boolean isTempBlobName(String blobName) {
        return blobName.startsWith(ChecksumBlobStoreFormat.TEMP_FILE_PREFIX);
    }

    protected BytesReference write(T obj) throws IOException {
        try (BytesStreamOutput bytesStreamOutput = new BytesStreamOutput()) {
            if (compress) {
                try (StreamOutput compressedStreamOutput = CompressorFactory.COMPRESSOR.streamOutput(bytesStreamOutput)) {
                    write(obj, compressedStreamOutput);
                }
            } else {
                write(obj, bytesStreamOutput);
            }
            return bytesStreamOutput.bytes();
        }
    }

    protected void write(T obj, StreamOutput streamOutput) throws IOException {
        try (XContentBuilder builder = XContentFactory.contentBuilder(xContentType, streamOutput)) {
            builder.startObject();
            obj.toXContent(builder, SNAPSHOT_ONLY_FORMAT_PARAMS);
            builder.endObject();
        }
    }


    protected String tempBlobName(String name) {
        return TEMP_FILE_PREFIX + String.format(Locale.ROOT, blobNameFormat, name);
    }

}
