/*
 * 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 com.android.printspooler.renderer;

import android.app.Service;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.pdf.PdfEditor;
import android.graphics.pdf.PdfRenderer;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintAttributes.Margins;
import android.util.Log;
import android.view.View;
import com.android.printspooler.util.PageRangeUtils;
import libcore.io.IoUtils;
import com.android.printspooler.util.BitmapSerializeUtils;
import java.io.IOException;

/**
 * Service for manipulation of PDF documents in an isolated process.
 */
public final class PdfManipulationService extends Service {
    public static final String ACTION_GET_RENDERER =
            "com.android.printspooler.renderer.ACTION_GET_RENDERER";
    public static final String ACTION_GET_EDITOR =
            "com.android.printspooler.renderer.ACTION_GET_EDITOR";

    public static final int ERROR_MALFORMED_PDF_FILE = -2;

    public static final int ERROR_SECURE_PDF_FILE = -3;

    private static final String LOG_TAG = "PdfManipulationService";
    private static final boolean DEBUG = false;

    private static final int MILS_PER_INCH = 1000;
    private static final int POINTS_IN_INCH = 72;

    @Override
    public IBinder onBind(Intent intent) {
        String action = intent.getAction();
        switch (action) {
            case ACTION_GET_RENDERER: {
                return new PdfRendererImpl();
            }
            case ACTION_GET_EDITOR: {
                return new PdfEditorImpl();
            }
            default: {
                throw new IllegalArgumentException("Invalid intent action:" + action);
            }
        }
    }

    private final class PdfRendererImpl extends IPdfRenderer.Stub {
        private final Object mLock = new Object();

        private Bitmap mBitmap;
        private PdfRenderer mRenderer;

        @Override
        public int openDocument(ParcelFileDescriptor source) throws RemoteException {
            synchronized (mLock) {
                try {
                    throwIfOpened();
                    if (DEBUG) {
                        Log.i(LOG_TAG, "openDocument()");
                    }
                    mRenderer = new PdfRenderer(source);
                    return mRenderer.getPageCount();
                } catch (IOException | IllegalStateException e) {
                    IoUtils.closeQuietly(source);
                    Log.e(LOG_TAG, "Cannot open file", e);
                    return ERROR_MALFORMED_PDF_FILE;
                } catch (SecurityException e) {
                    IoUtils.closeQuietly(source);
                    Log.e(LOG_TAG, "Cannot open file", e);
                    return ERROR_SECURE_PDF_FILE;
                }
            }
        }

        @Override
        public void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight,
                PrintAttributes attributes, ParcelFileDescriptor destination) {
            synchronized (mLock) {
                try {
                    throwIfNotOpened();

                    try (PdfRenderer.Page page = mRenderer.openPage(pageIndex)) {
                        final int srcWidthPts = page.getWidth();
                        final int srcHeightPts = page.getHeight();

                        final int dstWidthPts = pointsFromMils(
                                attributes.getMediaSize().getWidthMils());
                        final int dstHeightPts = pointsFromMils(
                                attributes.getMediaSize().getHeightMils());

                        final boolean scaleContent = mRenderer.shouldScaleForPrinting();
                        final boolean contentLandscape = !attributes.getMediaSize().isPortrait();

                        final float displayScale;
                        Matrix matrix = new Matrix();

                        if (scaleContent) {
                            displayScale = Math.min((float) bitmapWidth / srcWidthPts,
                                    (float) bitmapHeight / srcHeightPts);
                        } else {
                            if (contentLandscape) {
                                displayScale = (float) bitmapHeight / dstHeightPts;
                            } else {
                                displayScale = (float) bitmapWidth / dstWidthPts;
                            }
                        }
                        matrix.postScale(displayScale, displayScale);

                        Configuration configuration = PdfManipulationService.this.getResources()
                                .getConfiguration();
                        if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
                            matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0);
                        }

                        Margins minMargins = attributes.getMinMargins();
                        final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
                        final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
                        final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
                        final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());

                        Rect clip = new Rect();
                        clip.left = (int) (paddingLeftPts * displayScale);
                        clip.top = (int) (paddingTopPts * displayScale);
                        clip.right = (int) (bitmapWidth - paddingRightPts * displayScale);
                        clip.bottom = (int) (bitmapHeight - paddingBottomPts * displayScale);

                        if (DEBUG) {
                            Log.i(LOG_TAG, "Rendering page:" + pageIndex);
                        }

                        Bitmap bitmap = getBitmapForSize(bitmapWidth, bitmapHeight);
                        page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);

                        BitmapSerializeUtils.writeBitmapPixels(bitmap, destination);
                    }
                } catch (Throwable e) {
                    Log.e(LOG_TAG, "Cannot render page", e);

                    // The error is propagated to the caller when it tries to read the bitmap and
                    // the pipe is closed prematurely
                } finally {
                    IoUtils.closeQuietly(destination);
                }
            }
        }

        @Override
        public void closeDocument() {
            synchronized (mLock) {
                throwIfNotOpened();
                if (DEBUG) {
                    Log.i(LOG_TAG, "closeDocument()");
                }
                mRenderer.close();
                mRenderer = null;
            }
        }

        private Bitmap getBitmapForSize(int width, int height) {
            if (mBitmap != null) {
                if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) {
                    mBitmap.eraseColor(Color.WHITE);
                    return mBitmap;
                }
                mBitmap.recycle();
            }
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            mBitmap.eraseColor(Color.WHITE);
            return mBitmap;
        }

        private void throwIfOpened() {
            if (mRenderer != null) {
                throw new IllegalStateException("Already opened");
            }
        }

        private void throwIfNotOpened() {
            if (mRenderer == null) {
                throw new IllegalStateException("Not opened");
            }
        }
    }

    private final class PdfEditorImpl extends IPdfEditor.Stub {
        private final Object mLock = new Object();

        private PdfEditor mEditor;

        @Override
        public int openDocument(ParcelFileDescriptor source) throws RemoteException {
            synchronized (mLock) {
                try {
                    throwIfOpened();
                    if (DEBUG) {
                        Log.i(LOG_TAG, "openDocument()");
                    }
                    mEditor = new PdfEditor(source);
                    return mEditor.getPageCount();
                } catch (IOException | IllegalStateException e) {
                    IoUtils.closeQuietly(source);
                    Log.e(LOG_TAG, "Cannot open file", e);
                    throw new RemoteException(e.toString());
                }
            }
        }

        @Override
        public void removePages(PageRange[] ranges) {
            synchronized (mLock) {
                throwIfNotOpened();
                if (DEBUG) {
                    Log.i(LOG_TAG, "removePages()");
                }

                ranges = PageRangeUtils.normalize(ranges);

                int lastPageIdx = mEditor.getPageCount() - 1;

                final int rangeCount = ranges.length;
                for (int i = rangeCount - 1; i >= 0; i--) {
                    PageRange range = ranges[i];

                    // Ignore removal of pages that are outside the document
                    if (range.getEnd() > lastPageIdx) {
                        if (range.getStart() > lastPageIdx) {
                            continue;
                        }
                        range = new PageRange(range.getStart(), lastPageIdx);
                    }

                    for (int j = range.getEnd(); j >= range.getStart(); j--) {
                        mEditor.removePage(j);
                    }
                }
            }
        }

        @Override
        public void applyPrintAttributes(PrintAttributes attributes) {
            synchronized (mLock) {
                throwIfNotOpened();
                if (DEBUG) {
                    Log.i(LOG_TAG, "applyPrintAttributes()");
                }

                Rect mediaBox = new Rect();
                Rect cropBox = new Rect();
                Matrix transform = new Matrix();

                final boolean layoutDirectionRtl = getResources().getConfiguration()
                        .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;

                // We do not want to rotate the media box, so take into account orientation.
                final int dstWidthPts = pointsFromMils(attributes.getMediaSize().getWidthMils());
                final int dstHeightPts = pointsFromMils(attributes.getMediaSize().getHeightMils());

                final boolean scaleForPrinting = mEditor.shouldScaleForPrinting();

                final int pageCount = mEditor.getPageCount();
                for (int i = 0; i < pageCount; i++) {
                    if (!mEditor.getPageMediaBox(i, mediaBox)) {
                        Log.e(LOG_TAG, "Malformed PDF file");
                        return;
                    }

                    final int srcWidthPts = mediaBox.width();
                    final int srcHeightPts = mediaBox.height();

                    // Update the media box with the desired size.
                    mediaBox.right = dstWidthPts;
                    mediaBox.bottom = dstHeightPts;
                    mEditor.setPageMediaBox(i, mediaBox);

                    // Make sure content is top-left after media box resize.
                    transform.setTranslate(0, srcHeightPts - dstHeightPts);

                    // Scale the content if document allows it.
                    final float scale;
                    if (scaleForPrinting) {
                        scale = Math.min((float) dstWidthPts / srcWidthPts,
                                (float) dstHeightPts / srcHeightPts);
                        transform.postScale(scale, scale);
                    } else {
                        scale = 1.0f;
                    }

                    // Update the crop box relatively to the media box change, if needed.
                    if (mEditor.getPageCropBox(i, cropBox)) {
                        cropBox.left = (int) (cropBox.left * scale + 0.5f);
                        cropBox.top = (int) (cropBox.top * scale + 0.5f);
                        cropBox.right = (int) (cropBox.right * scale + 0.5f);
                        cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f);
                        cropBox.intersect(mediaBox);
                        mEditor.setPageCropBox(i, cropBox);
                    }

                    // If in RTL mode put the content in the logical top-right corner.
                    if (layoutDirectionRtl) {
                        final float dx = dstWidthPts - (int) (srcWidthPts * scale + 0.5f);
                        final float dy = 0;
                        transform.postTranslate(dx, dy);
                    }

                    // Adjust the physical margins if needed.
                    Margins minMargins = attributes.getMinMargins();
                    final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
                    final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
                    final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
                    final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());

                    Rect clip = new Rect(mediaBox);
                    clip.left += paddingLeftPts;
                    clip.top += paddingTopPts;
                    clip.right -= paddingRightPts;
                    clip.bottom -= paddingBottomPts;

                    // Apply the accumulated transforms.
                    mEditor.setTransformAndClip(i, transform, clip);
                }
            }
        }

        @Override
        public void write(ParcelFileDescriptor destination) throws RemoteException {
            synchronized (mLock) {
                try {
                    throwIfNotOpened();
                    if (DEBUG) {
                        Log.i(LOG_TAG, "write()");
                    }
                    mEditor.write(destination);
                } catch (IOException | IllegalStateException e) {
                    IoUtils.closeQuietly(destination);
                    Log.e(LOG_TAG, "Error writing PDF to file.", e);
                    throw new RemoteException(e.toString());
                }
            }
        }

        @Override
        public void closeDocument() {
            synchronized (mLock) {
                throwIfNotOpened();
                if (DEBUG) {
                    Log.i(LOG_TAG, "closeDocument()");
                }
                mEditor.close();
                mEditor = null;
            }
        }

        private void throwIfOpened() {
            if (mEditor != null) {
                throw new IllegalStateException("Already opened");
            }
        }

        private void throwIfNotOpened() {
            if (mEditor == null) {
                throw new IllegalStateException("Not opened");
            }
        }
    }

    private static int pointsFromMils(int mils) {
        return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH);
    }
}