/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed 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 android.graphics.pdf;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.ParcelFileDescriptor;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import dalvik.system.CloseGuard;
import libcore.io.IoUtils;

import java.io.IOException;

/**
 * Class for editing PDF files.
 *
 * @hide
 */
public final class PdfEditor {

    private final CloseGuard mCloseGuard = CloseGuard.get();

    private long mNativeDocument;

    private int mPageCount;

    private ParcelFileDescriptor mInput;

    /**
     * Creates a new instance.
     * <p>
     * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>,
     * i.e. its data being randomly accessed, e.g. pointing to a file. After finishing
     * with this class you must call {@link #close()}.
     * </p>
     * <p>
     * <strong>Note:</strong> This class takes ownership of the passed in file descriptor
     * and is responsible for closing it when the editor is closed.
     * </p>
     *
     * @param input Seekable file descriptor to read from.
     *
     * @throws java.io.IOException If an error occurs while reading the file.
     * @throws java.lang.SecurityException If the file requires a password or
     *         the security scheme is not supported.
     *
     * @see #close()
     */
    public PdfEditor(@NonNull ParcelFileDescriptor input) throws IOException {
        if (input == null) {
            throw new NullPointerException("input cannot be null");
        }

        final long size;
        try {
            Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET);
            size = Os.fstat(input.getFileDescriptor()).st_size;
        } catch (ErrnoException ee) {
            throw new IllegalArgumentException("file descriptor not seekable");
        }
        mInput = input;

        synchronized (PdfRenderer.sPdfiumLock) {
            mNativeDocument = nativeOpen(mInput.getFd(), size);
            try {
                mPageCount = nativeGetPageCount(mNativeDocument);
            } catch (Throwable t) {
                nativeClose(mNativeDocument);
                mNativeDocument = 0;
                throw t;
            }
        }

        mCloseGuard.open("close");
    }

    /**
     * Gets the number of pages in the document.
     *
     * @return The page count.
     */
    public int getPageCount() {
        throwIfClosed();
        return mPageCount;
    }

    /**
     * Removes the page with a given index.
     *
     * @param pageIndex The page to remove.
     */
    public void removePage(int pageIndex) {
        throwIfClosed();
        throwIfPageNotInDocument(pageIndex);

        synchronized (PdfRenderer.sPdfiumLock) {
            mPageCount = nativeRemovePage(mNativeDocument, pageIndex);
        }
    }

    /**
     * Sets a transformation and clip for a given page. The transformation matrix if
     * non-null must be affine as per {@link android.graphics.Matrix#isAffine()}. If
     * the clip is null, then no clipping is performed.
     *
     * @param pageIndex The page whose transform to set.
     * @param transform The transformation to apply.
     * @param clip The clip to apply.
     */
    public void setTransformAndClip(int pageIndex, @Nullable Matrix transform,
            @Nullable Rect clip) {
        throwIfClosed();
        throwIfPageNotInDocument(pageIndex);
        throwIfNotNullAndNotAfine(transform);
        if (transform == null) {
            transform = Matrix.IDENTITY_MATRIX;
        }
        if (clip == null) {
            Point size = new Point();
            getPageSize(pageIndex, size);

            synchronized (PdfRenderer.sPdfiumLock) {
                nativeSetTransformAndClip(mNativeDocument, pageIndex, transform.native_instance,
                        0, 0, size.x, size.y);
            }
        } else {
            synchronized (PdfRenderer.sPdfiumLock) {
                nativeSetTransformAndClip(mNativeDocument, pageIndex, transform.native_instance,
                        clip.left, clip.top, clip.right, clip.bottom);
            }
        }
    }

    /**
     * Gets the size of a given page in mils (1/72").
     *
     * @param pageIndex The page index.
     * @param outSize The size output.
     */
    public void getPageSize(int pageIndex, @NonNull Point outSize) {
        throwIfClosed();
        throwIfOutSizeNull(outSize);
        throwIfPageNotInDocument(pageIndex);

        synchronized (PdfRenderer.sPdfiumLock) {
            nativeGetPageSize(mNativeDocument, pageIndex, outSize);
        }
    }

    /**
     * Gets the media box of a given page in mils (1/72").
     *
     * @param pageIndex The page index.
     * @param outMediaBox The media box output.
     */
    public boolean getPageMediaBox(int pageIndex, @NonNull Rect outMediaBox) {
        throwIfClosed();
        throwIfOutMediaBoxNull(outMediaBox);
        throwIfPageNotInDocument(pageIndex);

        synchronized (PdfRenderer.sPdfiumLock) {
            return nativeGetPageMediaBox(mNativeDocument, pageIndex, outMediaBox);
        }
    }

    /**
     * Sets the media box of a given page in mils (1/72").
     *
     * @param pageIndex The page index.
     * @param mediaBox The media box.
     */
    public void setPageMediaBox(int pageIndex, @NonNull Rect mediaBox) {
        throwIfClosed();
        throwIfMediaBoxNull(mediaBox);
        throwIfPageNotInDocument(pageIndex);

        synchronized (PdfRenderer.sPdfiumLock) {
            nativeSetPageMediaBox(mNativeDocument, pageIndex, mediaBox);
        }
    }

    /**
     * Gets the crop box of a given page in mils (1/72").
     *
     * @param pageIndex The page index.
     * @param outCropBox The crop box output.
     */
    public boolean getPageCropBox(int pageIndex, @NonNull Rect outCropBox) {
        throwIfClosed();
        throwIfOutCropBoxNull(outCropBox);
        throwIfPageNotInDocument(pageIndex);

        synchronized (PdfRenderer.sPdfiumLock) {
            return nativeGetPageCropBox(mNativeDocument, pageIndex, outCropBox);
        }
    }

    /**
     * Sets the crop box of a given page in mils (1/72").
     *
     * @param pageIndex The page index.
     * @param cropBox The crop box.
     */
    public void setPageCropBox(int pageIndex, @NonNull Rect cropBox) {
        throwIfClosed();
        throwIfCropBoxNull(cropBox);
        throwIfPageNotInDocument(pageIndex);

        synchronized (PdfRenderer.sPdfiumLock) {
            nativeSetPageCropBox(mNativeDocument, pageIndex, cropBox);
        }
    }

    /**
     * Gets whether the document prefers to be scaled for printing.
     *
     * @return Whether to scale the document.
     */
    public boolean shouldScaleForPrinting() {
        throwIfClosed();

        synchronized (PdfRenderer.sPdfiumLock) {
            return nativeScaleForPrinting(mNativeDocument);
        }
    }

    /**
     * Writes the PDF file to the provided destination.
     * <p>
     * <strong>Note:</strong> This method takes ownership of the passed in file
     * descriptor and is responsible for closing it when writing completes.
     * </p>
     * @param output The destination.
     */
    public void write(ParcelFileDescriptor output) throws IOException {
        try {
            throwIfClosed();

            synchronized (PdfRenderer.sPdfiumLock) {
                nativeWrite(mNativeDocument, output.getFd());
            }
        } finally {
            IoUtils.closeQuietly(output);
        }
    }

    /**
     * Closes this editor. You should not use this instance
     * after this method is called.
     */
    public void close() {
        throwIfClosed();
        doClose();
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (mCloseGuard != null) {
                mCloseGuard.warnIfOpen();
            }

            doClose();
        } finally {
            super.finalize();
        }
    }

    private void doClose() {
        if (mNativeDocument != 0) {
            synchronized (PdfRenderer.sPdfiumLock) {
                nativeClose(mNativeDocument);
            }
            mNativeDocument = 0;
        }

        if (mInput != null) {
            IoUtils.closeQuietly(mInput);
            mInput = null;
        }
        mCloseGuard.close();
    }

    private void throwIfClosed() {
        if (mInput == null) {
            throw new IllegalStateException("Already closed");
        }
    }

    private void throwIfPageNotInDocument(int pageIndex) {
        if (pageIndex < 0 || pageIndex >= mPageCount) {
            throw new IllegalArgumentException("Invalid page index");
        }
    }

    private void throwIfNotNullAndNotAfine(Matrix matrix) {
        if (matrix != null && !matrix.isAffine()) {
            throw new IllegalStateException("Matrix must be afine");
        }
    }

    private void throwIfOutSizeNull(Point outSize) {
        if (outSize == null) {
            throw new NullPointerException("outSize cannot be null");
        }
    }

    private void throwIfOutMediaBoxNull(Rect outMediaBox) {
        if (outMediaBox == null) {
            throw new NullPointerException("outMediaBox cannot be null");
        }
    }

    private void throwIfMediaBoxNull(Rect mediaBox) {
        if (mediaBox == null) {
            throw new NullPointerException("mediaBox cannot be null");
        }
    }

    private void throwIfOutCropBoxNull(Rect outCropBox) {
        if (outCropBox == null) {
            throw new NullPointerException("outCropBox cannot be null");
        }
    }

    private void throwIfCropBoxNull(Rect cropBox) {
        if (cropBox == null) {
            throw new NullPointerException("cropBox cannot be null");
        }
    }

    private static native long nativeOpen(int fd, long size);
    private static native void nativeClose(long documentPtr);
    private static native int nativeGetPageCount(long documentPtr);
    private static native int nativeRemovePage(long documentPtr, int pageIndex);
    private static native void nativeWrite(long documentPtr, int fd);
    private static native void nativeSetTransformAndClip(long documentPtr, int pageIndex,
            long transformPtr, int clipLeft, int clipTop, int clipRight, int clipBottom);
    private static native void nativeGetPageSize(long documentPtr, int pageIndex, Point outSize);
    private static native boolean nativeGetPageMediaBox(long documentPtr, int pageIndex,
            Rect outMediaBox);
    private static native void nativeSetPageMediaBox(long documentPtr, int pageIndex,
            Rect mediaBox);
    private static native boolean nativeGetPageCropBox(long documentPtr, int pageIndex,
            Rect outMediaBox);
    private static native void nativeSetPageCropBox(long documentPtr, int pageIndex,
            Rect mediaBox);
    private static native boolean nativeScaleForPrinting(long documentPtr);
}
