/*
 * SVGPickScreen.java
 *
 * Copyright © 1998-2011 Research In Motion Limited
 * 
 * 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.
 *
 * Note: For the sake of simplicity, this sample application may not leverage
 * resource bundles and resource strings.  However, it is STRONGLY recommended
 * that application developers make use of the localization features available
 * within the BlackBerry development platform to ensure a seamless application
 * experience across a variety of languages and geographies.  For more information
 * on localizing your application, please refer to the BlackBerry Java Development
 * Environment Development Guide associated with this release.
 */

package com.rim.samples.device.svg.svgpickdemo;

import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;

import javax.microedition.m2g.SVGImage;
import javax.microedition.m2g.ScalableGraphics;
import javax.microedition.m2g.ScalableImage;

import net.rim.device.api.system.Display;
import net.rim.device.api.ui.Graphics;
import net.rim.device.api.ui.Screen;
import net.rim.device.api.ui.TouchEvent;
import net.rim.device.api.ui.container.MainScreen;
import net.rim.device.api.util.MathUtilities;

import org.w3c.dom.Document;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGLocatableElement;
import org.w3c.dom.svg.SVGMatrix;
import org.w3c.dom.svg.SVGRect;

/**
 * SVG cursor program to select from a group of selectable shapes and display
 * stats.
 * 
 * The attribute values of the SVG elements in sample.svg were hard-coded in a
 * way to display the image correctly on a 9500 device. However, one can
 * programmatically adjust those values by calling setFloatTrait() on the
 * SVGElement.
 */
public final class SVGPickScreen extends MainScreen {
    private static final int CURSOR_X_DEFAULT = 30;
    private static final int CURSOR_Y_DEFAULT = 60;
    private static final int CURSOR_MOVE_FACTOR = 10;

    private ScalableGraphics _scalableGraphics;
    private SVGImage _image;
    private Document _document;

    private int _cursorX;
    private int _cursorY;
    private SVGElement _cursor;
    private SVGElement _cursorPosition;

    private SVGElement _selectableShapesGroup;
    private final Vector _selectableShapes = new Vector();
    private SVGElement _selectedShapeBounds;

    private SVGElement _display;
    private SVGElement _displayName;
    private SVGElement _displayBounds;

    private int _displayWidth;
    private int _displayHeight;

    /**
     * Default constructor.
     */
    public SVGPickScreen() {
        setTitle("SVG Pick Demo");

        try {
            // Load SVG from svg
            final InputStream inputStream =
                    getClass().getResourceAsStream("/sample.svg");

            // Create SVGImage
            _image = (SVGImage) ScalableImage.createImage(inputStream, null);

            // Create Document
            _document = _image.getDocument();

            // Create ScalableGraphics Instance
            _scalableGraphics = ScalableGraphics.createInstance();

            _displayWidth = Display.getWidth();
            _displayHeight = Display.getHeight();

            // Initialize our view bindings.
            intializeElements();

            // Initial position of cursor point.
            updateCursor(CURSOR_X_DEFAULT, CURSOR_Y_DEFAULT);
        } catch (final IOException e) {
            System.exit(1);
        }
    }

    /**
     * Gets and initializes any view elements.
     */
    private void intializeElements() {
        // Get the cursor related elements.
        _cursor = (SVGElement) _document.getElementById("cursor");
        _cursorPosition =
                (SVGElement) _document.getElementById("cursorPosition");

        // Get the display elements which we will interact with
        _display = (SVGElement) _document.getElementById("display");
        _displayName = (SVGElement) _document.getElementById("displayName");
        _displayBounds = (SVGElement) _document.getElementById("displayBounds");

        // Get the selected shape elements and get the child elements of the
        // group
        // adding them to the array which is being managed.
        _selectedShapeBounds =
                (SVGElement) _document.getElementById("selectedShapeBounds");
        _selectableShapesGroup =
                (SVGElement) _document.getElementById("selectableShapesGroup");

        // Append all our selectable elements to the managed set.
        SVGLocatableElement element =
                (SVGLocatableElement) _selectableShapesGroup.getFirstChild();
        _selectableShapes.addElement(element);

        while ((element = (SVGLocatableElement) element.getNextSibling()) != null) {
            _selectableShapes.addElement(element);
        }
    }

    /**
     * @see net.rim.device.api.ui.Screen#touchEvent(TouchEvent)
     */
    protected boolean touchEvent(final TouchEvent message) {
        final int touchEvent = message.getEvent();
        if (touchEvent == TouchEvent.DOWN || touchEvent == TouchEvent.CLICK
                || touchEvent == TouchEvent.MOVE) {
            final int dx = message.getX(1) - _cursorX;
            final int dy = message.getY(1) - _cursorY;
            updateCursor(dx, dy);
        }

        return true;
    }

    /**
     * @see Screen#navigationMovement(int, int, int, int)
     */
    protected boolean navigationMovement(final int dx, final int dy,
            final int status, final int time) {
        final int deltaX = CURSOR_MOVE_FACTOR * dx;
        final int deltaY = CURSOR_MOVE_FACTOR * dy;

        // Update our new cursor position
        updateCursor(deltaX, deltaY);

        return true;
    }

    /**
     * Updates our cursor position.
     * 
     * @param deltaX
     *            The change in x of the cursor position
     * @param deltaY
     *            The change in y of the cursor position
     */
    private void updateCursor(final int deltaX, final int deltaY) {
        // Translate the cursor pointer.
        final int translateX = clampDelta(0, _displayWidth, _cursorX, deltaX);
        final int translateY = clampDelta(0, _displayHeight, _cursorY, deltaY);
        final SVGMatrix transform = _cursor.getMatrixTrait("transform");
        transform.mTranslate(translateX, translateY);
        _cursor.setMatrixTrait("transform", transform);

        // Update cursor position Text
        _cursorX = MathUtilities.clamp(0, _cursorX + translateX, _displayWidth);
        _cursorY =
                MathUtilities.clamp(0, _cursorY + translateY, _displayHeight);
        _cursorPosition.setTrait("#text", "" + _cursorX + ',' + _cursorY);

        // If a shape is selected then update the display.
        final SVGLocatableElement selected = findFirstSelectedShape();

        if (selected != null) {
            // Get the bounding box of selected object
            final SVGRect rect = selected.getBBox();

            // Update the display for the selected shape.
            updateDisplay(true, selected.getTrait("id"), rect);

            // Updated the bounding box for the selected shape.
            updateBounds(true, rect);
        } else {
            updateDisplay(false, null, null);
            updateBounds(false, null);
        }

        invalidate();

    }

    /**
     * Clamps the delta value to keep (current value + delta) value to between
     * the min and max bounds.
     * 
     * Note: The minimum bound should be less than, or equal to, the maximum
     * bound.
     * 
     * @param min
     *            The minimum bound
     * @param max
     *            The maximum bound
     * @param current
     *            The current value
     * @param delta
     *            The change in value
     * @return The new delta which will keep the (current + delta) value
     *         inbounds
     */
    private int clampDelta(final int min, final int max, final int current,
            final int delta) {
        final int currentAndDelta = current + delta;
        if (currentAndDelta < min) {
            return min - current;
        } else if (currentAndDelta > max) {
            return max - current;
        } else {
            return delta;
        }
    }

    /**
     * Searches for the first selected shape.
     * 
     * @return The shape that is located or <code>null</code> if no shape is
     *         found.
     */
    private SVGLocatableElement findFirstSelectedShape() {
        // Scroll through shapes, determine if cursor point
        // is within shape's bounding box, and update.
        SVGRect box;

        for (int i = 0; i < _selectableShapes.size(); i++) {
            final SVGLocatableElement element =
                    (SVGLocatableElement) _selectableShapes.elementAt(i);
            box = element.getBBox();

            // Do a bounding box to point test.
            if (_cursorX >= box.getX()
                    && _cursorX <= box.getX() + box.getWidth()
                    && _cursorY >= box.getY()
                    && _cursorY <= box.getY() + box.getHeight()) {
                return element;
            }
        }
        return null;
    }

    /**
     * Updates our display id and bounds region.
     * 
     * @param enabled
     *            <code>true</code> if the display is enabled.
     *            <code>false</code> if disabled.
     * @param id
     *            The id of the selected shape.
     * @param bounds
     *            The bounds of the selected shape.
     */
    private void updateDisplay(final boolean enabled, final String id,
            final SVGRect bounds) {
        if (enabled) {
            _display.setTrait("display", "inline");

            if (id != null) {
                // Update the id.
                _displayName.setTrait("#text", id);
            }

            if (bounds != null) {
                // Update the bounds region.
                final StringBuffer boundsText = new StringBuffer();
                boundsText.append(bounds.getX());
                boundsText.append(',');
                boundsText.append(bounds.getY());
                boundsText.append(',');
                boundsText.append(bounds.getWidth());
                boundsText.append(',');
                boundsText.append(bounds.getHeight());
                _displayBounds.setTrait("#text", boundsText.toString());
            }
        } else {
            _display.setTrait("display", "none");
        }
    }

    /**
     * Updates the selected shape bounding box.
     * 
     * @param enabled
     *            <code>true</code> if the display is enabled.
     *            <code>false</code> if disabled.
     * @param rect
     *            The rectangle whose bounds should be updated
     */
    private void updateBounds(final boolean enabled, final SVGRect rect) {
        if (enabled) {
            // Show the bounding box.
            _selectedShapeBounds.setTrait("display", "inline");

            if (rect != null) {
                // Updates the actually selected shapes bounding box.
                _selectedShapeBounds.setFloatTrait("x", rect.getX());
                _selectedShapeBounds.setFloatTrait("y", rect.getY());
                _selectedShapeBounds.setFloatTrait("width", rect.getWidth());
                _selectedShapeBounds.setFloatTrait("height", rect.getHeight());
            }
        } else {
            // Hide the bounding box.
            _selectedShapeBounds.setTrait("display", "none");
        }
    }

    /**
     * @see Screen#paint(Graphics)
     */
    protected void paint(final Graphics graphics) {
        super.paint(graphics);

        if (_image == null) {
            return;
        }

        // Bind target Graphics
        _scalableGraphics.bindTarget(graphics);

        // Set the viewport dimensions
        _image.setViewportWidth(_displayWidth);
        _image.setViewportHeight(_displayHeight);

        // Render the svg image/ model
        _scalableGraphics.render(0, 0, _image);

        // Release bindings on Graphics
        _scalableGraphics.releaseTarget();
    }

    /**
     * @see net.rim.device.api.ui.container.FullScreen#sublayout(int, int)
     */
    protected void sublayout(final int width, final int height) {
        if (_displayWidth != width || _displayHeight != height) {
            _displayWidth = width;
            _displayHeight = height;
        }

        super.sublayout(width, height);
    }
}
