/*
 * Copyright 2009, The Android Open Source Project
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "PaintPlugin.h"

#include <fcntl.h>
#include <math.h>
#include <string.h>

extern NPNetscapeFuncs*         browser;
extern ANPLogInterfaceV0        gLogI;
extern ANPCanvasInterfaceV0     gCanvasI;
extern ANPPaintInterfaceV0      gPaintI;
extern ANPPathInterfaceV0       gPathI;
extern ANPSurfaceInterfaceV0    gSurfaceI;
extern ANPSystemInterfaceV0     gSystemI;
extern ANPTypefaceInterfaceV0   gTypefaceI;
extern ANPWindowInterfaceV0     gWindowI;

///////////////////////////////////////////////////////////////////////////////

PaintPlugin::PaintPlugin(NPP inst) : SurfaceSubPlugin(inst) {

    m_isTouchActive = false;
    m_isTouchCurrentInput = true;
    m_activePaintColor = s_redColor;

    memset(&m_drawingSurface, 0, sizeof(m_drawingSurface));
    memset(&m_inputToggle,  0, sizeof(m_inputToggle));
    memset(&m_colorToggle, 0, sizeof(m_colorToggle));
    memset(&m_fullScreenToggle, 0, sizeof(m_fullScreenToggle));
    memset(&m_clearSurface,  0, sizeof(m_clearSurface));

    // initialize the drawing surface
    m_surface = NULL;

    // initialize the path
    m_touchPath = gPathI.newPath();
    if(!m_touchPath)
        gLogI.log(kError_ANPLogType, "----%p Unable to create the touch path", inst);

    // initialize the paint colors
    m_paintSurface = gPaintI.newPaint();
    gPaintI.setFlags(m_paintSurface, gPaintI.getFlags(m_paintSurface) | kAntiAlias_ANPPaintFlag);
    gPaintI.setColor(m_paintSurface, 0xFFC0C0C0);
    gPaintI.setTextSize(m_paintSurface, 18);

    m_paintButton = gPaintI.newPaint();
    gPaintI.setFlags(m_paintButton, gPaintI.getFlags(m_paintButton) | kAntiAlias_ANPPaintFlag);
    gPaintI.setColor(m_paintButton, 0xFFA8A8A8);

    // initialize the typeface (set the colors)
    ANPTypeface* tf = gTypefaceI.createFromName("serif", kItalic_ANPTypefaceStyle);
    gPaintI.setTypeface(m_paintSurface, tf);
    gTypefaceI.unref(tf);

    //register for touch events
    ANPEventFlags flags = kTouch_ANPEventFlag;
    NPError err = browser->setvalue(inst, kAcceptEvents_ANPSetValue, &flags);
    if (err != NPERR_NO_ERROR) {
        gLogI.log(kError_ANPLogType, "Error selecting input events.");
    }
}

PaintPlugin::~PaintPlugin() {
    gPathI.deletePath(m_touchPath);
    gPaintI.deletePaint(m_paintSurface);
    gPaintI.deletePaint(m_paintButton);

    setContext(NULL);
    destroySurface();
}

ANPCanvas* PaintPlugin::getCanvas(ANPRectI* dirtyRect) {

    ANPBitmap bitmap;
    JNIEnv* env = NULL;
    if (!m_surface || gVM->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK ||
        !gSurfaceI.lock(env, m_surface, &bitmap, dirtyRect)) {
            return NULL;
        }

    ANPCanvas* canvas = gCanvasI.newCanvas(&bitmap);

    // clip the canvas to the dirty rect b/c the surface is only required to
    // copy a minimum of the dirty rect and may copy more. The clipped canvas
    // however will never write to pixels outside of the clipped area.
    if (dirtyRect) {
        ANPRectF clipR;
        clipR.left = dirtyRect->left;
        clipR.top = dirtyRect->top;
        clipR.right = dirtyRect->right;
        clipR.bottom = dirtyRect->bottom;
        gCanvasI.clipRect(canvas, &clipR);
    }

    return canvas;
}

ANPCanvas* PaintPlugin::getCanvas(ANPRectF* dirtyRect) {

    ANPRectI newRect;
    newRect.left = (int) dirtyRect->left;
    newRect.top = (int) dirtyRect->top;
    newRect.right = (int) dirtyRect->right;
    newRect.bottom = (int) dirtyRect->bottom;

    return getCanvas(&newRect);
}

void PaintPlugin::releaseCanvas(ANPCanvas* canvas) {
    JNIEnv* env = NULL;
    if (m_surface && gVM->GetEnv((void**) &env, JNI_VERSION_1_4) == JNI_OK) {
        gSurfaceI.unlock(env, m_surface);
    }
    gCanvasI.deleteCanvas(canvas);
}

void PaintPlugin::drawCleanPlugin(ANPCanvas* canvas) {
    NPP instance = this->inst();
    PluginObject *obj = (PluginObject*) instance->pdata;

    // if no canvas get a locked canvas
    if (!canvas)
        canvas = getCanvas();

    if (!canvas)
        return;

    const float buttonWidth = 60;
    const float buttonHeight = 30;
    const int W = obj->window->width;
    const int H = obj->window->height;

    // color the plugin canvas
    gCanvasI.drawColor(canvas, 0xFFCDCDCD);

    // get font metrics
    ANPFontMetrics fontMetrics;
    gPaintI.getFontMetrics(m_paintSurface, &fontMetrics);

    // draw the input toggle button
    m_inputToggle.left = 5;
    m_inputToggle.top = H - buttonHeight - 5;
    m_inputToggle.right = m_inputToggle.left + buttonWidth;
    m_inputToggle.bottom = m_inputToggle.top + buttonHeight;
    gCanvasI.drawRect(canvas, &m_inputToggle, m_paintButton);
    const char* inputText = m_isTouchCurrentInput ? "Touch" : "Mouse";
    gCanvasI.drawText(canvas, inputText, strlen(inputText), m_inputToggle.left + 5,
                      m_inputToggle.top - fontMetrics.fTop, m_paintSurface);

    // draw the color selector button
    m_colorToggle.left = (W/3) - (buttonWidth/2);
    m_colorToggle.top = H - buttonHeight - 5;
    m_colorToggle.right = m_colorToggle.left + buttonWidth;
    m_colorToggle.bottom = m_colorToggle.top + buttonHeight;
    gCanvasI.drawRect(canvas, &m_colorToggle, m_paintButton);
    const char* colorText = getColorText();
    gCanvasI.drawText(canvas, colorText, strlen(colorText), m_colorToggle.left + 5,
                      m_colorToggle.top - fontMetrics.fTop, m_paintSurface);

    // draw the full-screen toggle button
    m_fullScreenToggle.left = ((W*2)/3) - (buttonWidth/2);
    m_fullScreenToggle.top = H - buttonHeight - 5;
    m_fullScreenToggle.right = m_fullScreenToggle.left + buttonWidth;
    m_fullScreenToggle.bottom = m_fullScreenToggle.top + buttonHeight;
    gCanvasI.drawRect(canvas, &m_fullScreenToggle, m_paintButton);
    const char* fullScreenText = "Full";
    gCanvasI.drawText(canvas, fullScreenText, strlen(fullScreenText),
                      m_fullScreenToggle.left + 5,
                      m_fullScreenToggle.top - fontMetrics.fTop, m_paintSurface);

    // draw the clear canvas button
    m_clearSurface.left = W - buttonWidth - 5;
    m_clearSurface.top = H - buttonHeight - 5;
    m_clearSurface.right = m_clearSurface.left + buttonWidth;
    m_clearSurface.bottom = m_clearSurface.top + buttonHeight;
    gCanvasI.drawRect(canvas, &m_clearSurface, m_paintButton);
    const char* clearText = "Clear";
    gCanvasI.drawText(canvas, clearText, strlen(clearText), m_clearSurface.left + 5,
                      m_clearSurface.top - fontMetrics.fTop, m_paintSurface);

    // draw the drawing surface box (5 px from the edge)
    m_drawingSurface.left = 5;
    m_drawingSurface.top = 5;
    m_drawingSurface.right = W - 5;
    m_drawingSurface.bottom = m_colorToggle.top - 5;
    gCanvasI.drawRect(canvas, &m_drawingSurface, m_paintSurface);

    // release the canvas
    releaseCanvas(canvas);
}

const char* PaintPlugin::getColorText() {

    if (m_activePaintColor == s_blueColor)
        return "Blue";
    else if (m_activePaintColor == s_greenColor)
        return "Green";
    else
        return "Red";
}

jobject PaintPlugin::getSurface() {
    if (m_surface) {
        return m_surface;
    }

    // load the appropriate java class and instantiate it
    JNIEnv* env = NULL;
    if (gVM->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        gLogI.log(kError_ANPLogType, " ---- getSurface: failed to get env");
        return NULL;
    }

    const char* className = "com.android.sampleplugin.PaintSurface";
    jclass paintClass = gSystemI.loadJavaClass(inst(), className);

    if(!paintClass) {
        gLogI.log(kError_ANPLogType, " ---- getSurface: failed to load class");
        return NULL;
    }

    PluginObject *obj = (PluginObject*) inst()->pdata;
    const int pW = obj->window->width;
    const int pH = obj->window->height;

    jmethodID constructor = env->GetMethodID(paintClass, "<init>", "(Landroid/content/Context;III)V");
    jobject paintSurface = env->NewObject(paintClass, constructor, m_context, (int)inst(), pW, pH);

    if(!paintSurface) {
        gLogI.log(kError_ANPLogType, " ---- getSurface: failed to construct object");
        return NULL;
    }

    m_surface = env->NewGlobalRef(paintSurface);
    return m_surface;
}

void PaintPlugin::destroySurface() {
    JNIEnv* env = NULL;
    if (m_surface && gVM->GetEnv((void**) &env, JNI_VERSION_1_4) == JNI_OK) {

        // detach the native code from the object
        jclass javaClass = env->GetObjectClass(m_surface);
        jmethodID invalMethod = env->GetMethodID(javaClass, "invalidateNPP", "()V");
        env->CallVoidMethod(m_surface, invalMethod);

        env->DeleteGlobalRef(m_surface);
        m_surface = NULL;
    }
}

int16 PaintPlugin::handleEvent(const ANPEvent* evt) {
    switch (evt->eventType) {
        case kTouch_ANPEventType: {
            float x = (float) evt->data.touch.x;
            float y = (float) evt->data.touch.y;
            if (kDown_ANPTouchAction == evt->data.touch.action && m_isTouchCurrentInput) {

                ANPRectF* rect = validTouch(evt->data.touch.x, evt->data.touch.y);
                if(rect == &m_drawingSurface) {
                    m_isTouchActive = true;
                    gPathI.moveTo(m_touchPath, x, y);
                    paintTouch();
                    return 1;
                }

            } else if (kMove_ANPTouchAction == evt->data.touch.action && m_isTouchActive) {
                gPathI.lineTo(m_touchPath, x, y);
                paintTouch();
                return 1;
            } else if (kUp_ANPTouchAction == evt->data.touch.action && m_isTouchActive) {
                gPathI.lineTo(m_touchPath, x, y);
                paintTouch();
                m_isTouchActive = false;
                gPathI.reset(m_touchPath);
                return 1;
            } else if (kCancel_ANPTouchAction == evt->data.touch.action) {
                m_isTouchActive = false;
                gPathI.reset(m_touchPath);
                return 1;
            } else if (kDoubleTap_ANPTouchAction == evt->data.touch.action) {
                gWindowI.requestCenterFitZoom(inst());
                return 1;

            }
            break;
        }
        case kMouse_ANPEventType: {

            if (m_isTouchActive)
                gLogI.log(kError_ANPLogType, "----%p Received unintended mouse event", inst());

            if (kDown_ANPMouseAction == evt->data.mouse.action) {
                ANPRectF* rect = validTouch(evt->data.mouse.x, evt->data.mouse.y);
                if (rect == &m_drawingSurface)
                    paintMouse(evt->data.mouse.x, evt->data.mouse.y);
                else if (rect == &m_inputToggle)
                    toggleInputMethod();
                else if (rect == &m_colorToggle)
                    togglePaintColor();
                else if (rect == &m_fullScreenToggle)
                    gWindowI.requestFullScreen(inst());
                else if (rect == &m_clearSurface)
                    drawCleanPlugin();
            }
            return 1;
        }
        case kCustom_ANPEventType: {

            switch (evt->data.other[0]) {
                case kSurfaceCreated_CustomEvent:
                    gLogI.log(kDebug_ANPLogType, " ---- customEvent: surfaceCreated");
                    /* The second draw call is added to cover up a problem in this
                       plugin and is not a recommended usage pattern. This plugin
                       does not correctly make partial updates to the double
                       buffered surface and this second call hides that problem.
                     */
                    drawCleanPlugin();
                    drawCleanPlugin();
                    break;
                case kSurfaceChanged_CustomEvent: {
                    gLogI.log(kDebug_ANPLogType, " ---- customEvent: surfaceChanged");

                    int width = evt->data.other[1];
                    int height = evt->data.other[2];

                    PluginObject *obj = (PluginObject*) inst()->pdata;
                    const int pW = obj->window->width;
                    const int pH = obj->window->height;
                    // compare to the plugin's surface dimensions
                    if (pW != width || pH != height)
                        gLogI.log(kError_ANPLogType,
                                  "----%p Invalid Surface Dimensions (%d,%d):(%d,%d)",
                                  inst(), pW, pH, width, height);
                    break;
                }
                case kSurfaceDestroyed_CustomEvent:
                    gLogI.log(kDebug_ANPLogType, " ---- customEvent: surfaceDestroyed");
                    break;
            }
            break; // end KCustom_ANPEventType
        }
        default:
            break;
    }
    return 0;   // unknown or unhandled event
}

ANPRectF* PaintPlugin::validTouch(int x, int y) {

    //convert to float
    float fx = (int) x;
    float fy = (int) y;

    if (fx > m_drawingSurface.left && fx < m_drawingSurface.right && fy > m_drawingSurface.top && fy < m_drawingSurface.bottom)
        return &m_drawingSurface;
    else if (fx > m_inputToggle.left && fx < m_inputToggle.right && fy > m_inputToggle.top && fy < m_inputToggle.bottom)
        return &m_inputToggle;
    else if (fx > m_colorToggle.left && fx < m_colorToggle.right && fy > m_colorToggle.top && fy < m_colorToggle.bottom)
        return &m_colorToggle;
    else if (fx > m_fullScreenToggle.left && fx < m_fullScreenToggle.right && fy > m_fullScreenToggle.top && fy < m_fullScreenToggle.bottom)
        return &m_fullScreenToggle;
    else if (fx > m_clearSurface.left && fx < m_clearSurface.right && fy > m_clearSurface.top && fy < m_clearSurface.bottom)
        return &m_clearSurface;
    else
        return NULL;
}

void PaintPlugin::toggleInputMethod() {
    m_isTouchCurrentInput = !m_isTouchCurrentInput;

    // lock only the input toggle and redraw the canvas
    ANPCanvas* lockedCanvas = getCanvas(&m_inputToggle);
    drawCleanPlugin(lockedCanvas);
}

void PaintPlugin::togglePaintColor() {
    if (m_activePaintColor == s_blueColor)
        m_activePaintColor = s_redColor;
    else if (m_activePaintColor == s_greenColor)
        m_activePaintColor = s_blueColor;
    else
        m_activePaintColor = s_greenColor;

    // lock only the color toggle and redraw the canvas
    ANPCanvas* lockedCanvas = getCanvas(&m_colorToggle);
    drawCleanPlugin(lockedCanvas);
}

void PaintPlugin::paintMouse(int x, int y) {
    //TODO do not paint outside the drawing surface

    //create the paint color
    ANPPaint* fillPaint = gPaintI.newPaint();
    gPaintI.setFlags(fillPaint, gPaintI.getFlags(fillPaint) | kAntiAlias_ANPPaintFlag);
    gPaintI.setStyle(fillPaint, kFill_ANPPaintStyle);
    gPaintI.setColor(fillPaint, m_activePaintColor);

    // handle the simple "mouse" paint (draw a point)
    ANPRectF point;
    point.left =   (float) x-3;
    point.top =    (float) y-3;
    point.right =  (float) x+3;
    point.bottom = (float) y+3;

    // get a canvas that is only locked around the point and draw it
    ANPCanvas* canvas = getCanvas(&point);
    gCanvasI.drawOval(canvas, &point, fillPaint);

    // clean up
    releaseCanvas(canvas);
    gPaintI.deletePaint(fillPaint);
}

void PaintPlugin::paintTouch() {
    //TODO do not paint outside the drawing surface

    //create the paint color
    ANPPaint* strokePaint = gPaintI.newPaint();
    gPaintI.setFlags(strokePaint, gPaintI.getFlags(strokePaint) | kAntiAlias_ANPPaintFlag);
    gPaintI.setColor(strokePaint, m_activePaintColor);
    gPaintI.setStyle(strokePaint, kStroke_ANPPaintStyle);
    gPaintI.setStrokeWidth(strokePaint, 6.0);
    gPaintI.setStrokeCap(strokePaint, kRound_ANPPaintCap);
    gPaintI.setStrokeJoin(strokePaint, kRound_ANPPaintJoin);

    // handle the complex "touch" paint (draw a line)
    ANPRectF bounds;
    gPathI.getBounds(m_touchPath, &bounds);

    // get a canvas that is only locked around the point and draw the path
    ANPCanvas* canvas = getCanvas(&bounds);
    gCanvasI.drawPath(canvas, m_touchPath, strokePaint);

    // clean up
    releaseCanvas(canvas);
    gPaintI.deletePaint(strokePaint);
}
