/*
 * Copyright (C) 2012 Google Inc.
 * Licensed to 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.mail.browse;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.MotionEvent;

import com.android.mail.R;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

public class ConversationWebView extends MailWebView implements ScrollNotifier {
    /** The initial delay when rendering in hardware layer. */
    private final int mWebviewInitialDelay;

    private Bitmap mBitmap;
    private Canvas mCanvas;

    private boolean mUseSoftwareLayer;
    /**
     * Whether this view is user-visible; we don't bother doing supplemental software drawing
     * if the view is off-screen.
     */
    private boolean mVisible;

    /** {@link Runnable} to be run when the page is rendered in hardware layer. */
    private final Runnable mNotifyPageRenderedInHardwareLayer = new Runnable() {
        @Override
        public void run() {
            // Switch to hardware layer.
            mUseSoftwareLayer = false;
            destroyBitmap();
            invalidate();
        }
    };

    @Override
    public void onDraw(Canvas canvas) {
        // Always render in hardware layer to avoid flicker when switch.
        super.onDraw(canvas);

        // Render in software layer on top if needed, and we're visible (i.e. it's worthwhile to
        // do all this)
        if (mUseSoftwareLayer && mVisible && getWidth() > 0 && getHeight() > 0) {
            if (mBitmap == null) {
                try {
                    // Create an offscreen bitmap.
                    mBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.RGB_565);
                    mCanvas = new Canvas(mBitmap);
                } catch (OutOfMemoryError e) {
                    // just give up
                    mBitmap = null;
                    mCanvas = null;
                    mUseSoftwareLayer = false;
                }
            }

            if (mBitmap != null) {
                final int x = getScrollX();
                final int y = getScrollY();

                mCanvas.save();
                mCanvas.translate(-x, -y);
                super.onDraw(mCanvas);
                mCanvas.restore();

                canvas.drawBitmap(mBitmap, x, y, null /* paint */);
            }
        }
    }

    @Override
    public void destroy() {
        destroyBitmap();
        removeCallbacks(mNotifyPageRenderedInHardwareLayer);

        super.destroy();
    }

    /**
     * Destroys the {@link Bitmap} used for software layer.
     */
    private void destroyBitmap() {
        if (mBitmap != null) {
            mBitmap = null;
            mCanvas = null;
        }
    }

    /**
     * Enable this WebView to also draw to an internal software canvas until
     * {@link #onRenderComplete()} is called. The software draw will happen every time
     * a normal {@link #onDraw(Canvas)} happens, and will overwrite whatever is normally drawn
     * (i.e. drawn in hardware) with the results of software rendering.
     * <p>
     * This is useful when you know that the WebView draws sooner to a software layer than it does
     * to its normal hardware layer.
     */
    public void setUseSoftwareLayer(boolean useSoftware) {
        mUseSoftwareLayer = useSoftware;
    }

    /**
     * Notifies the {@link ConversationWebView} that it has become visible. It can use this signal
     * to switch between software and hardware layer.
     */
    public void onRenderComplete() {
        if (mUseSoftwareLayer) {
            // Schedule to switch from software layer to hardware layer in 1s.
            postDelayed(mNotifyPageRenderedInHardwareLayer, mWebviewInitialDelay);
        }
    }

    public void onUserVisibilityChanged(boolean visible) {
        mVisible = visible;
    }

    private final int mViewportWidth;
    private final float mDensity;

    private final Set<ScrollListener> mScrollListeners =
            new CopyOnWriteArraySet<ScrollListener>();

    /**
     * True when WebView is handling a touch-- in between POINTER_DOWN and
     * POINTER_UP/POINTER_CANCEL.
     */
    private boolean mHandlingTouch;
    private boolean mIgnoringTouch;

    private static final String LOG_TAG = LogTag.getLogTag();

    public ConversationWebView(Context c) {
        this(c, null);
    }

    public ConversationWebView(Context c, AttributeSet attrs) {
        super(c, attrs);

        final Resources r = getResources();
        mViewportWidth = r.getInteger(R.integer.conversation_webview_viewport_px);
        mWebviewInitialDelay = r.getInteger(R.integer.webview_initial_delay);
        mDensity = r.getDisplayMetrics().density;
    }

    @Override
    public void addScrollListener(ScrollListener l) {
        mScrollListeners.add(l);
    }

    @Override
    public void removeScrollListener(ScrollListener l) {
        mScrollListeners.remove(l);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        for (ScrollListener listener : mScrollListeners) {
            listener.onNotifierScroll(t);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getActionMasked();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mHandlingTouch = true;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                LogUtils.d(LOG_TAG, "WebView disabling intercepts: POINTER_DOWN");
                requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mHandlingTouch = false;
                mIgnoringTouch = false;
                break;
        }

        final boolean handled = mIgnoringTouch || super.onTouchEvent(ev);

        return handled;
    }

    public boolean isHandlingTouch() {
        return mHandlingTouch;
    }

    public int getViewportWidth() {
        return mViewportWidth;
    }

    /**
     * Returns the effective width available for HTML content in DP units. This width takes into
     * account the given margin (in screen px) by excluding it. This is not the same as DOM width,
     * since the document is rendered at CSS px={@link #mViewportWidth}.
     *
     * @param sideMarginPx HTML body margin, if any (in screen px)
     * @return width available for HTML content (in dp)
     */
    public int getWidthInDp(int sideMarginPx) {
        return (int) ((getWidth() - sideMarginPx * 2) / mDensity);
    }

    /**
     * Similar to {@link #getScale()}, except that it returns the initially expected scale, as
     * determined by the ratio of actual screen pixels to logical HTML pixels.
     * <p>This assumes that we are able to control the logical HTML viewport with a meta-viewport
     * tag.
     */
    public float getInitialScale() {
        final float scale;
        if (getSettings().getLoadWithOverviewMode()) {
            // in overview mode (aka auto-fit mode), the base ratio is screen px : viewport px
            scale = (float) getWidth() / getViewportWidth();
        } else {
            // in no-zoom mode, the base ratio is just screen px : mdpi css px (i.e. density)
            scale = mDensity;
        }
        return scale;
    }

    public int screenPxToWebPx(int screenPx) {
        return (int) (screenPx / getInitialScale());
    }

    public int webPxToScreenPx(int webPx) {
        return (int) (webPx * getInitialScale());
    }

    public float screenPxToWebPxError(int screenPx) {
        return screenPx / getInitialScale() - screenPxToWebPx(screenPx);
    }

    public float webPxToScreenPxError(int webPx) {
        return webPx * getInitialScale() - webPxToScreenPx(webPx);
    }

}
