/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * 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.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "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 THE COPYRIGHT OWNER 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.
 */

package com.jme3.input.android;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.opengl.GLSurfaceView;
import android.view.Surface;
import android.view.WindowManager;
import com.jme3.input.AbstractJoystick;
import com.jme3.input.DefaultJoystickAxis;
import com.jme3.input.InputManager;
import com.jme3.input.JoyInput;
import com.jme3.input.Joystick;
import com.jme3.input.JoystickAxis;
import com.jme3.input.SensorJoystickAxis;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.math.FastMath;
import com.jme3.util.IntMap;
import com.jme3.util.IntMap.Entry;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * AndroidSensorJoyInput converts the Android Sensor system into Joystick events.
 * A single joystick is configured and includes data for all configured sensors
 * as seperate axes of the joystick.
 *
 * Each axis is named according to the static strings in SensorJoystickAxis.
 * Refer to the strings defined in SensorJoystickAxis for a list of supported
 * sensors and their axis data.  Each sensor type defined in SensorJoystickAxis
 * will be attempted to be configured.  If the device does not support a particular
 * sensor, the axis will return null if joystick.getAxis(String name) is called.
 *
 * The joystick.getXAxis and getYAxis methods of the joystick are configured to
 * return the device orientation values in the device's X and Y directions.
 *
 * @author iwgeric
 */
public class AndroidSensorJoyInput implements SensorEventListener {
    private final static Logger logger = Logger.getLogger(AndroidSensorJoyInput.class.getName());

    private AndroidJoyInput joyInput;
    private SensorManager sensorManager = null;
    private WindowManager windowManager = null;
    private IntMap<SensorData> sensors = new IntMap<SensorData>();
    private int lastRotation = 0;
    private boolean loaded = false;

    public AndroidSensorJoyInput(AndroidJoyInput joyInput) {
        this.joyInput = joyInput;
    }

    /**
     * Internal class to enclose data for each sensor.
     */
    private class SensorData {
        int androidSensorType = -1;
        int androidSensorSpeed = SensorManager.SENSOR_DELAY_GAME;
        Sensor sensor = null;
        int sensorAccuracy = -1;
        float[] lastValues;
        final Object valuesLock = new Object();
        ArrayList<AndroidSensorJoystickAxis> axes = new ArrayList<AndroidSensorJoystickAxis>();
        boolean enabled = false;
        boolean haveData = false;

        public SensorData(int androidSensorType, Sensor sensor) {
            this.androidSensorType = androidSensorType;
            this.sensor = sensor;
        }

    }

    public void setView(GLSurfaceView view) {
        pauseSensors();
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
        if (view == null) {
            windowManager = null;
            sensorManager = null;
        } else {
            // Get instance of the WindowManager from the current Context
            windowManager = (WindowManager) view.getContext().getSystemService(Context.WINDOW_SERVICE);
            // Get instance of the SensorManager from the current Context
            sensorManager = (SensorManager) view.getContext().getSystemService(Context.SENSOR_SERVICE);
        }
    }

    private SensorData initSensor(int sensorType) {
        boolean success = false;

        SensorData sensorData = sensors.get(sensorType);
        if (sensorData != null) {
            unRegisterListener(sensorType);
        } else {
            sensorData = new SensorData(sensorType, null);
            sensors.put(sensorType, sensorData);
        }

        sensorData.androidSensorType = sensorType;
        sensorData.sensor = sensorManager.getDefaultSensor(sensorType);

        if (sensorData.sensor != null) {
            logger.log(Level.FINE, "Sensor Type {0} found.", sensorType);
            success = registerListener(sensorType);
        } else {
            logger.log(Level.FINE, "Sensor Type {0} not found.", sensorType);
        }

        if (success) {
            return sensorData;
        } else {
            return null;
        }
    }

    private boolean registerListener(int sensorType) {
        SensorData sensorData = sensors.get(sensorType);
        if (sensorData != null) {
            if (sensorData.enabled) {
                logger.log(Level.FINE, "Sensor Already Active: SensorType: {0}, active: {1}",
                        new Object[]{sensorType, sensorData.enabled});
                return true;
            }
            sensorData.haveData = false;
            if (sensorData.sensor != null) {
                if (sensorManager.registerListener(this, sensorData.sensor, sensorData.androidSensorSpeed)) {
                    sensorData.enabled = true;
                    logger.log(Level.FINE, "SensorType: {0}, actived: {1}",
                            new Object[]{sensorType, sensorData.enabled});
                    return true;
                } else {
                    sensorData.enabled = false;
                    logger.log(Level.FINE, "Sensor Type {0} activation failed.", sensorType);
                }
            }
        }
        return false;
    }

    private void unRegisterListener(int sensorType) {
        SensorData sensorData = sensors.get(sensorType);
        if (sensorData != null) {
            if (sensorData.sensor != null) {
                sensorManager.unregisterListener(this, sensorData.sensor);
            }
            sensorData.enabled = false;
            sensorData.haveData = false;
            logger.log(Level.FINE, "SensorType: {0} deactivated, active: {1}",
                    new Object[]{sensorType, sensorData.enabled});
        }
    }

    /**
     * Pauses the sensors to save battery life if the sensors are not needed.
     * Used to pause sensors when the activity pauses
     */
    public void pauseSensors() {
        for (Entry entry: sensors) {
            if (entry.getKey() != Sensor.TYPE_ORIENTATION) {
                unRegisterListener(entry.getKey());
            }
        }
    }

    /**
     * Resumes the sensors.
     * Used to resume sensors when the activity comes to the top of the stack
     */
    public void resumeSensors() {
        for (Entry entry: sensors) {
            if (entry.getKey() != Sensor.TYPE_ORIENTATION) {
                registerListener(entry.getKey());
            }
        }
    }

    /*
     * Allows the orientation data to be rotated based on the current device
     * rotation.  This keeps the data aligned with the game when the user
     * rotates the device during game play.
     *
     * Android remapCoordinateSystem from the Android docs
     * remapCoordinateSystem(float[] inR, int X, int Y, float[] outR)
     *
     * @param   inR   the rotation matrix to be transformed. Usually it is the matrix
     *          returned by getRotationMatrix(float[], float[], float[], float[]).
     *
     * @param   outR  the transformed rotation matrix. inR and outR can be the same
     *          array, but it is not recommended for performance reason.
     *
     * X     defines on which world (Earth) axis and direction the X axis of the device is mapped.
     * Y     defines on which world (Earth) axis and direction the Y axis of the device is mapped.
     *
     * @return True if successful
     */
    private boolean remapCoordinates(float[] inR, float[] outR) {
        int xDir = SensorManager.AXIS_X;
        int yDir = SensorManager.AXIS_Y;
        int curRotation = getScreenRotation();
        if (lastRotation != curRotation) {
            logger.log(Level.FINE, "Device Rotation changed to: {0}", curRotation);
        }
        lastRotation = curRotation;

//        logger.log(Level.FINE, "Screen Rotation: {0}", getScreenRotation());
        switch (getScreenRotation()) {
            // device natural position
            case Surface.ROTATION_0:
                xDir = SensorManager.AXIS_X;
                yDir = SensorManager.AXIS_Y;
                break;
            // device rotated 90 deg counterclockwise
            case Surface.ROTATION_90:
                xDir = SensorManager.AXIS_Y;
                yDir = SensorManager.AXIS_MINUS_X;
                break;
            // device rotated 180 deg counterclockwise
            case Surface.ROTATION_180:
                xDir = SensorManager.AXIS_MINUS_X;
                yDir = SensorManager.AXIS_MINUS_Y;
                break;
            // device rotated 270 deg counterclockwise
            case Surface.ROTATION_270:
                xDir = SensorManager.AXIS_MINUS_Y;
                yDir = SensorManager.AXIS_X;
                break;
            default:
                break;
        }
        return SensorManager.remapCoordinateSystem(inR, xDir, yDir, outR);
    }

    /**
     * Returns the current device rotation.
     * Surface.ROTATION_0 = device in natural default rotation
     * Surface.ROTATION_90 = device in rotated 90deg counterclockwise
     * Surface.ROTATION_180 = device in rotated 180deg counterclockwise
     * Surface.ROTATION_270 = device in rotated 270deg counterclockwise
     *
     * When the Manifest locks the orientation, this value will not change during
     * gametime, but if the orientation of the screen is based off the sensor,
     * this value will change as the device is rotated.
     * @return Current device rotation amount
     */
    private int getScreenRotation() {
        return windowManager.getDefaultDisplay().getRotation();
    }

    /**
     * Calculates the device orientation based off the data recieved from the
     * Acceleration Sensor and Mangetic Field sensor
     * Values are returned relative to the Earth.
     *
     * From the Android Doc
     *
     * Computes the device's orientation based on the rotation matrix. When it returns, the array values is filled with the result:
     *  values[0]: azimuth, rotation around the Z axis.
     *  values[1]: pitch, rotation around the X axis.
     *  values[2]: roll, rotation around the Y axis.
     *
     * The reference coordinate-system used is different from the world
     * coordinate-system defined for the rotation matrix:
     *  X is defined as the vector product Y.Z (It is tangential to the ground at the device's current location and roughly points West).
     *  Y is tangential to the ground at the device's current location and points towards the magnetic North Pole.
     *  Z points towards the center of the Earth and is perpendicular to the ground.
     *
     * @return True if Orientation was calculated
     */
    private boolean updateOrientation() {
        SensorData sensorData;
        AndroidSensorJoystickAxis axis;
        final float[] curInclinationMat = new float[16];
        final float[] curRotationMat = new float[16];
        final float[] rotatedRotationMat = new float[16];
        final float[] accValues = new float[3];
        final float[] magValues = new float[3];
        final float[] orderedOrientation = new float[3];

        // if the Gravity Sensor is available, use it for orientation, if not
        // use the accelerometer
        // NOTE: Seemed to work worse, so just using accelerometer
//        sensorData = sensors.get(Sensor.TYPE_GRAVITY);
//        if (sensorData == null) {
            sensorData = sensors.get(Sensor.TYPE_ACCELEROMETER);
//        }

        if (sensorData == null || !sensorData.enabled || !sensorData.haveData) {
            return false;
        }

        if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
            return false;
        }

        synchronized(sensorData.valuesLock) {
            accValues[0] = sensorData.lastValues[0];
            accValues[1] = sensorData.lastValues[1];
            accValues[2] = sensorData.lastValues[2];
        }

        sensorData = sensors.get(Sensor.TYPE_MAGNETIC_FIELD);
        if (sensorData == null || !sensorData.enabled || !sensorData.haveData) {
            return false;
        }

        if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
            return false;
        }

        synchronized(sensorData.valuesLock) {
            magValues[0] = sensorData.lastValues[0];
            magValues[1] = sensorData.lastValues[1];
            magValues[2] = sensorData.lastValues[2];
        }

        if (SensorManager.getRotationMatrix(curRotationMat, curInclinationMat, accValues, magValues)) {
            final float [] orientValues = new float[3];
            if (remapCoordinates(curRotationMat, rotatedRotationMat)) {
                SensorManager.getOrientation(rotatedRotationMat, orientValues);
//                logger.log(Level.FINE, "Orientation Values: {0}, {1}, {2}",
//                        new Object[]{orientValues[0], orientValues[1], orientValues[2]});


                // need to reorder to make it x, y, z order instead of z, x, y order
                orderedOrientation[0] = orientValues[1];
                orderedOrientation[1] = orientValues[2];
                orderedOrientation[2] = orientValues[0];

                sensorData = sensors.get(Sensor.TYPE_ORIENTATION);
                if (sensorData != null && sensorData.axes.size() > 0) {
                    for (int i=0; i<orderedOrientation.length; i++) {
                        axis = sensorData.axes.get(i);
                        if (axis != null) {
                            axis.setCurRawValue(orderedOrientation[i]);
                            if (!sensorData.haveData) {
                                sensorData.haveData = true;
                            } else {
                                if (axis.isChanged()) {
                                    joyInput.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
                                }
                            }
                        }
                    }
                } else if (sensorData != null) {
                    if (!sensorData.haveData) {
                        sensorData.haveData = true;
                    }
                }

                return true;
            } else {
                logger.log(Level.FINE, "remapCoordinateSystem failed");
            }

        } else {
            logger.log(Level.FINE, "getRotationMatrix returned false");
        }

        return false;
    }

    // Start of JoyInput methods

    public Joystick loadJoystick(int joyId, InputManager inputManager) {
        SensorData sensorData;
        AndroidSensorJoystickAxis axis;

        AndroidSensorJoystick joystick = new AndroidSensorJoystick(inputManager,
                                    joyInput,
                                    joyId,
                                    "AndroidSensorsJoystick");

        List<Sensor> availSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
        for (Sensor sensor: availSensors) {
            logger.log(Level.FINE, "{0} Sensor is available, Type: {1}, Vendor: {2}, Version: {3}",
                    new Object[]{sensor.getName(), sensor.getType(), sensor.getVendor(), sensor.getVersion()});
        }

        // manually create orientation sensor data since orientation is not a physical sensor
        sensorData = new SensorData(Sensor.TYPE_ORIENTATION, null);
        sensorData.lastValues = new float[3];
        sensors.put(Sensor.TYPE_ORIENTATION, sensorData);
        axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_X, SensorJoystickAxis.ORIENTATION_X, joystick.getAxisCount(), FastMath.HALF_PI);
        joystick.setYAxis(axis); // joystick y axis = rotation around device x axis
        sensorData.axes.add(axis);
        axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_Y, SensorJoystickAxis.ORIENTATION_Y, joystick.getAxisCount(), FastMath.HALF_PI);
        joystick.setXAxis(axis); // joystick x axis = rotation around device y axis
        sensorData.axes.add(axis);
        axis = joystick.addAxis(SensorJoystickAxis.ORIENTATION_Z, SensorJoystickAxis.ORIENTATION_Z, joystick.getAxisCount(), FastMath.HALF_PI);
        sensorData.axes.add(axis);

        // add axes for physical sensors
        sensorData = initSensor(Sensor.TYPE_MAGNETIC_FIELD);
        if (sensorData != null) {
            sensorData.lastValues = new float[3];
            sensors.put(Sensor.TYPE_MAGNETIC_FIELD, sensorData);
//            axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_X, "MagneticField_X", joystick.getAxisCount(), 1f);
//            sensorData.axes.add(axis);
//            axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_Y, "MagneticField_Y", joystick.getAxisCount(), 1f);
//            sensorData.axes.add(axis);
//            axis = joystick.addAxis(SensorJoystickAxis.MAGNETIC_Z, "MagneticField_Z", joystick.getAxisCount(), 1f);
//            sensorData.axes.add(axis);
        }

        sensorData = initSensor(Sensor.TYPE_ACCELEROMETER);
        if (sensorData != null) {
            sensorData.lastValues = new float[3];
            sensors.put(Sensor.TYPE_ACCELEROMETER, sensorData);
//            axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_X, "Accelerometer_X", joystick.getAxisCount(), 1f);
//            sensorData.axes.add(axis);
//            axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_Y, "Accelerometer_Y", joystick.getAxisCount(), 1f);
//            sensorData.axes.add(axis);
//            axis = joystick.addAxis(SensorJoystickAxis.ACCELEROMETER_Z, "Accelerometer_Z", joystick.getAxisCount(), 1f);
//            sensorData.axes.add(axis);
        }

//        sensorData = initSensor(Sensor.TYPE_GYROSCOPE);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[3];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_GRAVITY);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[3];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_LINEAR_ACCELERATION);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[3];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_ROTATION_VECTOR);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[4];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_PROXIMITY);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[1];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_LIGHT);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[1];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_PRESSURE);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[1];
//        }
//
//        sensorData = initSensor(Sensor.TYPE_TEMPERATURE);
//        if (sensorData != null) {
//            sensorData.lastValues = new float[1];
//        }


        loaded = true;
        return joystick;
    }

    public void update() {
        if (!loaded) {
            return;
        }
        updateOrientation();
    }

    public void destroy() {
        logger.log(Level.FINE, "Doing Destroy.");
        pauseSensors();
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
        sensors.clear();
        loaded = false;
        sensorManager = null;
    }

    // Start of Android SensorEventListener methods

    @Override
    public void onSensorChanged(SensorEvent se) {
        if (!loaded) {
            return;
        }
//        logger.log(Level.FINE, "onSensorChanged for {0}: accuracy: {1}, values: {2}",
//                new Object[]{se.sensor.getName(), se.accuracy, se.values});

        int sensorType = se.sensor.getType();

        SensorData sensorData = sensors.get(sensorType);
        if (sensorData != null) {
//            logger.log(Level.FINE, "sensorData name: {0}, enabled: {1}, unreliable: {2}",
//                    new Object[]{sensorData.sensor.getName(), sensorData.enabled, sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE});
        }
        if (sensorData != null && sensorData.sensor.equals(se.sensor) && sensorData.enabled) {

            if (sensorData.sensorAccuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) {
                return;
            }
            synchronized(sensorData.valuesLock) {
                for (int i=0; i<sensorData.lastValues.length; i++) {
                    sensorData.lastValues[i] = se.values[i];
                }
            }

            if (sensorData.axes.size() > 0) {
                AndroidSensorJoystickAxis axis;
                for (int i=0; i<se.values.length; i++) {
                    axis = sensorData.axes.get(i);
                    if (axis != null) {
                        axis.setCurRawValue(se.values[i]);
                        if (!sensorData.haveData) {
                            sensorData.haveData = true;
                        } else {
                            if (axis.isChanged()) {
                                JoyAxisEvent event = new JoyAxisEvent(axis, axis.getJoystickAxisValue());
//                                logger.log(Level.INFO, "adding JoyAxisEvent: {0}", event);
                                joyInput.addEvent(event);
//                                joyHandler.addEvent(new JoyAxisEvent(axis, axis.getJoystickAxisValue()));
                            }
                        }
                    }
                }
            } else if (sensorData != null) {
                if (!sensorData.haveData) {
                    sensorData.haveData = true;
                }
            }

        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
        int sensorType = sensor.getType();
        SensorData sensorData = sensors.get(sensorType);
        if (sensorData != null) {
            logger.log(Level.FINE, "onAccuracyChanged for {0}: accuracy: {1}",
                    new Object[]{sensor.getName(), i});
            logger.log(Level.FINE, "MaxRange: {0}, Resolution: {1}",
                    new Object[]{sensor.getMaximumRange(), sensor.getResolution()});
            sensorData.sensorAccuracy = i;
        }
    }

    // End of SensorEventListener methods

    protected class AndroidSensorJoystick extends AbstractJoystick {
        private JoystickAxis nullAxis;
        private JoystickAxis xAxis;
        private JoystickAxis yAxis;
        private JoystickAxis povX;
        private JoystickAxis povY;

        public AndroidSensorJoystick( InputManager inputManager, JoyInput joyInput,
                                int joyId, String name){

            super( inputManager, joyInput, joyId, name );

            this.nullAxis = new DefaultJoystickAxis( getInputManager(), this, -1,
                                                     "Null", "null", false, false, 0 );
            this.xAxis = nullAxis;
            this.yAxis = nullAxis;
            this.povX = nullAxis;
            this.povY = nullAxis;

        }

        protected AndroidSensorJoystickAxis addAxis(String axisName, String logicalName, int axisNum, float maxRawValue) {
            AndroidSensorJoystickAxis axis;

            axis = new AndroidSensorJoystickAxis(
                    getInputManager(),          // InputManager (InputManager)
                    this,                       // parent Joystick (Joystick)
                    axisNum,                    // Axis Index (int)
                    axisName,                   // Axis Name (String)
                    logicalName,                // Logical ID (String)
                    true,                       // isAnalog (boolean)
                    false,                      // isRelative (boolean)
                    0.01f,                      // Axis Deadzone (float)
                    maxRawValue);               // Axis Max Raw Value (float)

            super.addAxis(axis);

            return axis;
        }

        protected void setXAxis(JoystickAxis axis) {
            xAxis = axis;
        }
        protected void setYAxis(JoystickAxis axis) {
            yAxis = axis;
        }

        @Override
        public JoystickAxis getXAxis() {
            return xAxis;
        }

        @Override
        public JoystickAxis getYAxis() {
            return yAxis;
        }

        @Override
        public JoystickAxis getPovXAxis() {
            return povX;
        }

        @Override
        public JoystickAxis getPovYAxis() {
            return povY;
        }

    }

    public class AndroidSensorJoystickAxis extends DefaultJoystickAxis implements SensorJoystickAxis {
        float zeroRawValue = 0f;
        float curRawValue = 0f;
        float lastRawValue = 0f;
        boolean hasChanged = false;
        float maxRawValue = FastMath.HALF_PI;
        boolean enabled = true;

        public AndroidSensorJoystickAxis(InputManager inputManager, Joystick parent,
                           int axisIndex, String name, String logicalId,
                           boolean isAnalog, boolean isRelative, float deadZone,
                           float maxRawValue) {
            super(inputManager, parent, axisIndex, name, logicalId, isAnalog, isRelative, deadZone);

            this.maxRawValue = maxRawValue;
        }

        @Override
        public float getMaxRawValue() {
            return maxRawValue;
        }

        @Override
        public void setMaxRawValue(float maxRawValue) {
            this.maxRawValue = maxRawValue;
        }

        protected float getLastRawValue() {
            return lastRawValue;
        }
        protected void setCurRawValue(float rawValue) {
            this.curRawValue = rawValue;
            if (Math.abs(curRawValue - lastRawValue) > getDeadZone()) {
                hasChanged = true;
                lastRawValue = curRawValue;
            } else {
                hasChanged = false;
            }
        }

        protected float getJoystickAxisValue() {
            return (lastRawValue-zeroRawValue) / maxRawValue;
        }

        protected boolean isChanged() {
            return hasChanged;
        }

        @Override
        public void calibrateCenter() {
            zeroRawValue = lastRawValue;
            logger.log(Level.FINE, "Calibrating axis {0} to {1}",
                    new Object[]{getName(), zeroRawValue});
        }

    }
}
