/**
 *
 *   Copyright (C) 2014 Luke Deighton
 *
 *   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.lukedeighton.wheelview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.SystemClock;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.InflateException;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;

import com.lukedeighton.wheelview.adapter.WheelAdapter;
import com.lukedeighton.wheelview.transformer.FadingSelectionTransformer;
import com.lukedeighton.wheelview.transformer.ScalingItemTransformer;
import com.lukedeighton.wheelview.transformer.WheelItemTransformer;
import com.lukedeighton.wheelview.transformer.WheelSelectionTransformer;

import java.util.ArrayList;
import java.util.List;

//TODO onWheelItemSelected callback for when the wheel has settled (0 angular velocity), and one when it is passed
//TODO empty - physics to spring away - prevent movement out from edge
//TODO sticky selection - always settle on a state that completely selects an item
//TODO circular clip option?
//TODO Saving State during screen rotate etc. SavedState extends BaseSavedState
//TODO can items be rendered as views or use recyclerView - use viewgroup?
//TODO onWheelItemVisibilityChange needs to factor in when items are cycled within view bounds and should that have another callback?
//TODO option to get wheel state (either flinging or dragging)
//TODO item radius works separately ? uses min angle etc. to figure out in the layout event
//TODO setWheelVelocity method
//TODO util method to animate to a wheel position?

public class WheelView extends View {

    private static final Rect sTempRect = new Rect();

    private static final float VELOCITY_FRICTION_COEFFICIENT = 0.015f;
    private static final float CONSTANT_FRICTION_COEFFICIENT = 0.0028f;
    private static final float ANGULAR_VEL_COEFFICIENT = 22f;
    private static final float MAX_ANGULAR_VEL = 0.3f;

    private static final int LEFT_MASK = 0x01;
    private static final int RIGHT_MASK = 0x02;
    private static final int TOP_MASK = 0x04;
    private static final int BOTTOM_MASK = 0x08;

    private static final int NEVER_USED = 0;

    //The touch factors decrease the drag movement towards the center of the wheel. It is there so
    //that dragging the wheel near the center doesn't cause the wheel's angle to change
    //drastically. It is squared to provide a linear function once multiplied by 1/r^2
    private static final int TOUCH_FACTOR_SIZE = 20;
    private static final float TOUCH_DRAG_COEFFICIENT = 0.8f;

    private static final float[] TOUCH_FACTORS;
    static {
        int size = TOUCH_FACTOR_SIZE;
        TOUCH_FACTORS = new float[size];
        int maxIndex = size - 1;
        float numerator = size * size;
        for (int i = 0; i < size; i++) {
            int factor = maxIndex - i + 1;
            TOUCH_FACTORS[i] = (1 - factor * factor / numerator) * TOUCH_DRAG_COEFFICIENT;
        }
    }

    private static final float CLICK_MAX_DRAGGED_ANGLE = 0.7f;

    private static final CacheItem EMPTY_CACHE_ITEM = new CacheItem(true);

    private VelocityTracker mVelocityTracker;
    private Vector mForceVector = new Vector();
    private Vector mRadiusVector = new Vector();
    private float mAngle;
    private float mAngularVelocity;
    private long mLastUpdateTime;
    private boolean mRequiresUpdate;
    private int mRawSelectedPosition;
    private float mLastWheelTouchX;
    private float mLastWheelTouchY;

    private CacheItem[] mItemCacheArray;
    private Drawable mWheelDrawable;
    private Drawable mEmptyItemDrawable;
    private Drawable mSelectionDrawable;

    private boolean mIsRepeatable;
    private boolean mIsWheelDrawableRotatable = true;

    /**
     * The item angle is the angle covered per item on the wheel and is in degrees.
     * The {@link #mItemAnglePadding} is included in the item angle.
     */
    private float mItemAngle;

    /**
     * Angle padding is in degrees and reduces the wheel's items size during layout
     */
    private float mItemAnglePadding;

    /**
     * Selection Angle is the angle at which an item is considered selected.
     * The {@link #mOnItemSelectListener} is called when the 'most selected' item changes.
     */
    private float mSelectionAngle;

    private int mSelectionPadding;

    private int mWheelPadding;
    private int mWheelToItemDistance;
    private int mItemRadius;
    private int mWheelRadius;
    private int mOffsetX;
    private int mOffsetY;
    private int mItemCount;

    private int mWheelPosition;
    private int mLeft, mTop, mWidth, mHeight;
    private Rect mViewBounds = new Rect();
    private Circle mWheelBounds;

    /**
     * Wheel item bounds are always pre-rotation and based on the {@link #mSelectionAngle}
     */
    private List<Circle> mWheelItemBounds;

    /**
     * The ItemState contain the rotated position
     */
    private List<ItemState> mItemStates;
    private int mAdapterItemCount;

    private boolean mIsDraggingWheel;
    private float mLastTouchAngle;
    private ItemState mClickedItem;
    private float mDraggedAngle;

    private OnWheelItemClickListener mOnItemClickListener;
    private OnWheelAngleChangeListener mOnAngleChangeListener;
    private OnWheelItemSelectListener mOnItemSelectListener;
    private OnWheelItemVisibilityChangeListener mOnItemVisibilityChangeListener;
    private WheelItemTransformer mItemTransformer;
    private WheelSelectionTransformer mSelectionTransformer;
    private WheelAdapter mAdapter;

    public WheelView(Context context) {
        super(context);
        initWheelView();
    }

    public WheelView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WheelView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initWheelView();

        //TODO possible pattern to follow from android source
        /* final int N = a.getIndexCount();
        for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break; */

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WheelView, defStyle, 0);

        Drawable d = a.getDrawable(R.styleable.WheelView_emptyItemDrawable);
        if (d != null) {
            setEmptyItemDrawable(d);
        } else if (a.hasValue(R.styleable.WheelView_emptyItemColor)) {
            int color = a.getColor(R.styleable.WheelView_emptyItemColor, NEVER_USED);
            setEmptyItemColor(color);
        }

        d = a.getDrawable(R.styleable.WheelView_wheelDrawable);
        if (d != null) {
            setWheelDrawable(d);
        } else if (a.hasValue(R.styleable.WheelView_wheelColor)){
            int color = a.getColor(R.styleable.WheelView_wheelColor, NEVER_USED);
            setWheelColor(color);
        }

        d = a.getDrawable(R.styleable.WheelView_selectionDrawable);
        if (d != null) {
            setSelectionDrawable(d);
        } else if (a.hasValue(R.styleable.WheelView_selectionColor)) {
            int color = a.getColor(R.styleable.WheelView_selectionColor, NEVER_USED);
            setSelectionColor(color);
        }

        mSelectionPadding = a.getDimensionPixelSize(R.styleable.WheelView_selectionPadding, 0);
        mIsRepeatable = a.getBoolean(R.styleable.WheelView_repeatItems, false);
        mIsWheelDrawableRotatable = a.getBoolean(R.styleable.WheelView_rotatableWheelDrawable, true);
        mSelectionAngle = a.getFloat(R.styleable.WheelView_selectionAngle, 0f);
        setWheelRadius(a.getLayoutDimension(R.styleable.WheelView_wheelRadius, 0 /* TODO Wrap_content */));
        mOffsetX = a.getDimensionPixelSize(R.styleable.WheelView_wheelOffsetX, 0);
        mOffsetY = a.getDimensionPixelSize(R.styleable.WheelView_wheelOffsetY, 0);
        mWheelToItemDistance = a.getLayoutDimension(R.styleable.WheelView_wheelToItemDistance, ViewGroup.LayoutParams.MATCH_PARENT);

        int itemCount = a.getInteger(R.styleable.WheelView_wheelItemCount, 0);

        //TODO maybe just remove angle padding?
        mItemAnglePadding = a.getFloat(R.styleable.WheelView_wheelItemAnglePadding, 0f); //TODO angle works with the ItemRadius

        if (itemCount != 0) {
            setWheelItemCount(itemCount);
        } else {
            float itemAngle = a.getFloat(R.styleable.WheelView_wheelItemAngle, 0f);
            if (itemAngle != 0f) {
                setWheelItemAngle(itemAngle);
            }
        }

        mItemRadius = a.getDimensionPixelSize(R.styleable.WheelView_wheelItemRadius, 0);

        if (mItemCount == 0 && mWheelToItemDistance > 0 && mWheelRadius > 0) {
            mItemAngle = calculateAngle(mWheelRadius, mWheelToItemDistance) + mItemAnglePadding;
            setWheelItemAngle(mItemAngle);
        }

        String itemTransformerStr = a.getString(R.styleable.WheelView_wheelItemTransformer);
        if (itemTransformerStr != null) {
            mItemTransformer = validateAndInstantiate(itemTransformerStr, WheelItemTransformer.class);
        }

        String selectionTransformerStr = a.getString(R.styleable.WheelView_selectionTransformer);
        if (selectionTransformerStr != null) {
            mSelectionTransformer = validateAndInstantiate(selectionTransformerStr, WheelSelectionTransformer.class);
        }

        mWheelPadding = a.getDimensionPixelSize(R.styleable.WheelView_wheelPadding, 0);

        mWheelPosition = a.getInt(R.styleable.WheelView_wheelPosition, 0);
        if (!a.hasValue(R.styleable.WheelView_selectionAngle)) {
            //TODO use gravity to default the selection angle if not already specified
        }

        a.recycle();
    }

    @SuppressWarnings("unchecked")
    private <T> T validateAndInstantiate(String clazzName, Class<? extends T> clazz) {
        String errorMessage;
        T instance;
        try {
            Class<?> xmlClazz = Class.forName(clazzName);
            if (clazz.isAssignableFrom(xmlClazz)) {
                try {
                    errorMessage = null;
                    instance = (T) xmlClazz.newInstance();
                } catch (InstantiationException e) {
                    errorMessage = "No argumentless constructor for " + xmlClazz.getSimpleName();
                    instance = null;
                } catch (IllegalAccessException e) {
                    errorMessage = "The argumentless constructor is not public for " + xmlClazz.getSimpleName();
                    instance = null;
                }
            } else {
                errorMessage = "Class inflated from xml (" + xmlClazz.getSimpleName() + ") does not implement " + clazz.getSimpleName();
                instance = null;
            }
        } catch (ClassNotFoundException e) {
            errorMessage = clazzName + " class was not found when inflating from xml";
            instance = null;
        }

        if (errorMessage != null) {
            throw new InflateException(errorMessage);
        } else {
            return instance;
        }
    }

    private boolean hasMask(int value, int mask) {
        return (value & mask) == mask;
    }

    public boolean isPositionLeft() {
        return hasMask(mWheelPosition, LEFT_MASK);
    }

    public boolean isPositionRight() {
        return hasMask(mWheelPosition, RIGHT_MASK);
    }

    public boolean isPositionTop() {
        return hasMask(mWheelPosition, TOP_MASK);
    }

    public boolean isPositionBottom() {
        return hasMask(mWheelPosition, BOTTOM_MASK);
    }

    public void initWheelView() {
        //TODO I only really need to init with default values if there are non defined from attributes...
        mItemTransformer = new ScalingItemTransformer();
        mSelectionTransformer = new FadingSelectionTransformer();
    }

    public interface OnWheelItemClickListener {
        void onWheelItemClick(WheelView parent, int position, boolean isSelected);
    }

    public void setOnWheelItemClickListener(OnWheelItemClickListener listener) {
        mOnItemClickListener = listener;
    }

    public OnWheelItemClickListener getOnWheelItemClickListener() {
        return mOnItemClickListener;
    }

    /**
     * A listener for when a wheel item is selected.
     */
    public interface OnWheelItemSelectListener {
        /**
         * @param parent WheelView that calls this listener
         * @param itemDrawable - The Drawable of the wheel item that is closest to the selection angle
         *                     (or closest to the selection angle)
         * @param position of the adapter that is closest to the selection angle
         */
        void onWheelItemSelected(WheelView parent, Drawable itemDrawable, int position);

        //TODO onWheelItemSettled?
    }

    public void setOnWheelItemSelectedListener(OnWheelItemSelectListener listener) {
        mOnItemSelectListener = listener;
    }

    public OnWheelItemSelectListener getOnWheelItemSelectListener() {
        return mOnItemSelectListener;
    }

    public interface OnWheelItemVisibilityChangeListener {
        void onItemVisibilityChange(WheelAdapter adapter, int position, boolean isVisible);
    }

    /* TODO public */ void setOnWheelItemVisibilityChangeListener(OnWheelItemVisibilityChangeListener listener) {
        mOnItemVisibilityChangeListener = listener;
    }

    public OnWheelItemVisibilityChangeListener getOnItemVisibilityChangeListener() {
        return mOnItemVisibilityChangeListener;
    }

    /**
     * A listener for when the wheel's angle has changed.
     */
    public interface OnWheelAngleChangeListener {
        /**
         * Receive a callback when the wheel's angle has changed.
         */
        void onWheelAngleChange(float angle);
    }

    public void setOnWheelAngleChangeListener(OnWheelAngleChangeListener listener) {
        mOnAngleChangeListener = listener;
    }

    public OnWheelAngleChangeListener getOnWheelAngleChangeListener() {
        return mOnAngleChangeListener;
    }

    public void setAdapter(WheelAdapter wheelAdapter) {
        mAdapter = wheelAdapter;
        int count = mAdapter.getCount();
        mItemCacheArray = new CacheItem[count];
        mAdapterItemCount = count;
        invalidate();
    }

    public WheelAdapter getAdapter() {
        return mAdapter;
    }

    public void setWheelItemTransformer(WheelItemTransformer itemTransformer) {
        if (itemTransformer == null) throw new IllegalArgumentException("WheelItemTransformer cannot be null");
        mItemTransformer = itemTransformer;
    }

    public void setWheelSelectionTransformer(WheelSelectionTransformer transformer) {
        mSelectionTransformer = transformer;
    }

    /**
     * <p> When true the wheel drawable is rotated as well as the wheel items.
     * For performance it is better to not rotate the wheel drawable if possible.
     * <p> The default value is true
     */
    public void setWheelDrawableRotatable(boolean isWheelDrawableRotatable) {
        mIsWheelDrawableRotatable = isWheelDrawableRotatable;
        invalidate();
    }

    /**
     * @return {@code true} if the wheel drawable rotates.
     */
    public boolean isWheelDrawableRotatable() {
        return mIsWheelDrawableRotatable;
    }

    /**
     * Set Repeatable Adapter to true will continuously cycle through the set of adapter items.
     */
    public void setRepeatableAdapter(boolean isRepeatable) {
        mIsRepeatable = isRepeatable;
    }

    /**
     * @return {@code true} if the adapter items continuously cycle around the wheel.
     */
    public boolean isRepeatableAdapter() {
        return mIsRepeatable;
    }

    public void setWheelItemAngle(float angle) {
        mItemAngle = angle + mItemAnglePadding;
        mItemCount = calculateItemCount(mItemAngle);
        //TODO mItemRadius = calculateWheelItemRadius(mItemAngle);

        if (mWheelBounds != null) {
            invalidate();
        }

        //TODO
    }

    public float getWheelItemAngle() {
        return mItemAngle;
    }

    private float calculateItemAngle(int itemCount) {
        return 360f / itemCount;
    }

    private int calculateItemCount(float angle) {
        return (int) (360f / angle);
    }

    public void setWheelItemAnglePadding(float anglePadding) {
        mItemAnglePadding = anglePadding;

        //TODO
    }

    public float getWheelItemAnglePadding() {
        return mItemAnglePadding;
    }

    public void setSelectionAngle(float angle) {
        mSelectionAngle = Circle.clamp180(angle);

        if (mWheelBounds != null) {
            layoutWheelItems();
        }
    }

    public float getSelectionAngle() {
        return mSelectionAngle;
    }

    public void setSelectionPadding(int padding) {
        mSelectionPadding = padding;
    }

    public int getSelectionPadding() {
        return mSelectionPadding;
    }

    public void setWheelToItemDistance(int distance) {
        mWheelToItemDistance = distance;
    }

    public float getWheelToItemDistance() {
        return mWheelToItemDistance;
    }

    public void setWheelItemRadius(int radius) {
        mItemRadius = radius;
    }

    /* TODO
    public void setWheelItemRadius(float radius, int itemCount) {
        mItemRadius = radius;
        mItemAngle = calculateItemAngle(itemCount);
        mItemCount = itemCount;
    } */

    public float getWheelItemRadius() {
        return mItemRadius;
    }

    /**
     * Sets the wheel radius in pixels.
     */
    public void setWheelRadius(int radius) {
        if (radius < -1) throw new IllegalArgumentException("Invalid Wheel Radius: " + radius);

        mWheelRadius = radius;
    }

    /**
     * Gets the radius of the wheel in pixels
     */
    public float getWheelRadius() {
        return mWheelRadius;
    }

    /**
     * Sets the number of items to be displayed on the wheel.
     */
    public void setWheelItemCount(int count) {
        mItemCount = count;
        mItemAngle = calculateItemAngle(count);

        if (mWheelBounds != null) {
            invalidate();
            //TODO ?
        }
    }

    /**
     * @return the count of wheel items that are displayed on the wheel.
     */
    public float getWheelItemCount() {
        return mItemCount;
    }

    public void setWheelOffsetX(int offsetX) {
        mOffsetX = offsetX;
        //TODO
    }

    public float getWheelOffsetX() {
        return mOffsetX;
    }

    public void setWheelOffsetY(int offsetY) {
        mOffsetY = offsetY;
        //TODO
    }

    public float getWheelOffsetY() {
        return mOffsetY;
    }

    /*
    public void setWheelPosition(int position) {
        //TODO possible solution to animate or instantly?
    }*/

    /**
     * Find the largest circle to fit within the item angle.
     * The point of intersection occurs at a tangent to the wheel item.
     */
    private float calculateWheelItemRadius(float angle) {
        return (float) (mWheelToItemDistance * Math.sin(Math.toRadians((double) ((angle - mItemAnglePadding) / 2f))));
    }

    private float calculateAngle(float innerRadius, float outerRadius) {
        return 2f * (float) Math.toDegrees(Math.asin((double) (innerRadius / outerRadius)));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int width = right - left;
        int height = bottom - top;

        if (mWidth != width || mHeight != height || mLeft != left || mTop != top) {
            layoutWheel(0, 0, width, height);
        }

        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //if we are not to measure exactly then check what size we would like to be
        int desiredWidth;
        if (widthMode != MeasureSpec.EXACTLY) {
            if (mWheelRadius >= 0) {
                desiredWidth = mWheelRadius * 2 + getPaddingLeft() + getPaddingRight();
            } else {
                desiredWidth = widthSize;
            }
        } else {
            desiredWidth = -1;
        }

        int desiredHeight;
        if (heightMode != MeasureSpec.EXACTLY) {
            if (mWheelRadius >= 0) {
                desiredHeight = mWheelRadius * 2 + getPaddingTop() + getPaddingBottom();
            } else {
                desiredHeight = heightSize;
            }
        } else {
            desiredHeight = -1;
        }

        desiredWidth = Math.max(desiredWidth, getSuggestedMinimumWidth());
        desiredHeight = Math.max(desiredHeight, getSuggestedMinimumHeight());

        int width = resolveSizeAndState(desiredWidth, widthMeasureSpec);
        int height = resolveSizeAndState(desiredHeight, heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    //Taken and modified from Android Source for API < 11
    public static int resolveSizeAndState(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize =  MeasureSpec.getSize(measureSpec);
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    result = specSize;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private void layoutWheel(int left, int top, int width, int height) {
        if (width == 0 || height == 0) return;

        mLeft = left;
        mTop = top;
        mWidth = width;
        mHeight = height;

        mViewBounds.set(left, top, left + width, top + height);
        setWheelBounds(width, height);

        layoutWheelItems();
    }

    private void setWheelBounds(int width, int height) {
        float relativeVertical = 0.5f, relativeHorizontal = 0.5f;
        if (isPositionLeft()) relativeHorizontal -= 0.5f;
        if (isPositionRight()) relativeHorizontal += 0.5f;
        if (isPositionTop()) relativeVertical -= 0.5f;
        if (isPositionBottom()) relativeVertical += 0.5f;

        final int centerX = (int) (mOffsetX + width * relativeHorizontal);
        final int centerY = (int) (mOffsetY + height * relativeVertical);

        int wheelRadius = measureWheelRadius(mWheelRadius, width, height);
        mWheelBounds = new Circle(centerX, centerY, wheelRadius);

        if (mWheelDrawable != null) {
            mWheelDrawable.setBounds(mWheelBounds.getBoundingRect());
        }
    }

    private int measureWheelRadius(int radius, int width, int height) {
        if (radius == ViewGroup.LayoutParams.MATCH_PARENT) {
            return Math.min(width - getPaddingLeft() - getPaddingRight(),
                            height - getPaddingTop() - getPaddingBottom()) / 2;
        } else {
            return radius;
        }
    }

    private void layoutWheelItems() {
        mItemStates = new ArrayList<ItemState>(mItemCount);
        for (int i = 0; i < mItemCount; i++) {
            mItemStates.add(new ItemState());
        }

        if (mWheelItemBounds == null) {
            mWheelItemBounds = new ArrayList<Circle>(mItemCount);
        } else if (!mWheelItemBounds.isEmpty()) {
            mWheelItemBounds.clear();
        }

        if (mWheelToItemDistance == ViewGroup.LayoutParams.MATCH_PARENT) {
            mWheelToItemDistance = (int) (mWheelBounds.mRadius - mItemRadius - mWheelPadding);
        }

        float itemAngleRadians = (float) Math.toRadians(mItemAngle);
        float offsetRadians = (float) Math.toRadians(-mSelectionAngle);
        for (int i = 0; i < mItemCount; i++) {
            float angle = itemAngleRadians * i + offsetRadians;
            float x = mWheelBounds.mCenterX + mWheelToItemDistance * (float) Math.cos(angle);
            float y = mWheelBounds.mCenterY + mWheelToItemDistance * (float) Math.sin(angle);
            mWheelItemBounds.add(new Circle(x, y, mItemRadius));
        }

        invalidate();
    }

    /**
     * You should set the wheel drawable not to rotate for a performance benefit.
     * See the method {@link #setWheelDrawableRotatable(boolean)}
     */
    public void setWheelColor(int color) {
        setWheelDrawable(createOvalDrawable(color));
    }

    /**
     * Sets the wheel's drawable that can also rotate with the items.
     *
     * @see #setWheelDrawableRotatable(boolean)
     * @see #setWheelDrawable(int)
     */
    public void setWheelDrawable(@DrawableRes int resId) {
        setWheelDrawable(getResources().getDrawable(resId));
    }

    /**
     * <p>
     * Sets the wheel's drawable that can also rotate with the items.
     * </p>
     * <p>
     * Note if the drawable has infinite lines of symmetry then you should set the wheel drawable to
     * not rotate, see {@link #setWheelDrawableRotatable(boolean)}. In other words, if the drawable
     * doesn't look any different whilst it is rotating, you should improve the performance by
     * disabling the drawable from rotating.
     * </p>
     *
     * @see #setWheelDrawableRotatable(boolean)
     */
    public void setWheelDrawable(Drawable drawable) {
        mWheelDrawable = drawable;

        if (mWheelBounds != null) {
            mWheelDrawable.setBounds(mWheelBounds.getBoundingRect());
            invalidate();
        }
    }

    /**
     * Sets the empty item drawable that is drawn when outside of the adapter range.
     *
     * @see #isEmptyItemPosition(int)
     */
    public void setEmptyItemColor(int color) {
        setEmptyItemDrawable(createOvalDrawable(color));
    }

    /**
     * Sets the empty item drawable that is drawn when outside of the adapter range.
     *
     * @see #isEmptyItemPosition(int)
     */
    public void setEmptyItemDrawable(@DrawableRes int resId) {
        setEmptyItemDrawable(getResources().getDrawable(resId));
    }

    /**
     * Sets the empty item drawable that is drawn when outside of the adapter range.
     *
     * @see #isEmptyItemPosition(int)
     */
    public void setEmptyItemDrawable(Drawable drawable) {
        mEmptyItemDrawable = drawable;
        EMPTY_CACHE_ITEM.mDrawable = drawable;

        if (mWheelBounds != null) {
            invalidate();
        }
    }

    /**
     * Sets the selection drawable to be a circular color
     *
     * @see #setSelectionDrawable(int)
     * @see #setSelectionDrawable(Drawable)
     */
    public void setSelectionColor(int color) {
        setSelectionDrawable(createOvalDrawable(color));
    }

    /**
     * Sets the selection drawable from a Drawable Resource.
     *
     * @see #setSelectionColor(int)
     * @see #setSelectionDrawable(Drawable)
     */
    public void setSelectionDrawable(@DrawableRes int resId) {
        setSelectionDrawable(getResources().getDrawable(resId));
    }

    /**
     * Set the selection drawable that is drawn behind the selected item.
     *
     * @see #setSelectionDrawable(int)
     * @see #setSelectionColor(int)
     */
    public void setSelectionDrawable(Drawable drawable) {
        mSelectionDrawable = drawable;
        invalidate();
    }

    /**
     * @return The Drawable that is drawn behind the selected item.
     */
    public Drawable getSelectionDrawable() {
        return mSelectionDrawable;
    }

    /**
     * @return the empty item drawable used when rendering positions outside of the adapter range.
     *
     * @see #isEmptyItemPosition(int)
     */
    public Drawable getEmptyItemDrawable() {
        return mEmptyItemDrawable;
    }

    /**
     * @return the wheel's drawable
     */
    public Drawable getWheelDrawable() {
        return mWheelDrawable;
    }

    /**
     * @return the absolute angle for the item at the given position
     */
    public float getAngleForPosition(int rawPosition) {
        return rawPosition * mItemAngle;
    }

    /**
     * <p>
     * Changes the wheel angle so that the item at the provided position becomes selected.
     * </p>
     * <p>
     * Note that this does not change the selection angle, instead it will rotate the wheel
     * to the angle where the provided position becomes selected.
     * </p>
     *
     * @param rawPosition the raw position (can take negative numbers)
     *
     * @see #setMidSelected()
     */
    public void setSelected(int rawPosition) {
        //must rotate the wheel in the opposite direction so that the given position becomes selected
        setAngle(-1f * getAngleForPosition(rawPosition));
    }

    /**
     * Changes the wheel angle so that the item in the middle of the adapter becomes selected.
     *
     * @see #setSelected(int)
     */
    public void setMidSelected() {
        if (mAdapter == null || mAdapterItemCount == 0)
            throw new IllegalStateException("Cannot select position with no adapter items");

        setSelected(mAdapterItemCount / 2);
    }

    /**
     * The raw selected position (can be negative and isn't cyclic)
     *
     * @see #getAngle()
     * @see #getSelectedPosition()
     */
    public int getRawSelectedPosition() {
        return mRawSelectedPosition;
    }

    /**
     * Set the angle of the wheel instantaneously.
     * Note this does not animate to the provided angle.
     *
     * @param angle given in degrees and can be any value (not only between 0 and 360)
     */
    public void setAngle(float angle) {
        mAngle = angle;

        updateSelectedPosition();

        if (mOnAngleChangeListener != null) {
            mOnAngleChangeListener.onWheelAngleChange(mAngle);
        }

        invalidate();
    }

    /**
     * Checks to see if the selectedPosition has changed.
     */
    private void updateSelectedPosition() {
        int position = (int) ((-mAngle + -0.5 * Math.signum(mAngle) * mItemAngle) / mItemAngle);
        setSelectedPosition(position);
    }

    /**
     * @return {@code true} if this adapter position is empty.
     *
     * This is only possible with non-repeatable items.
     */
    public boolean isEmptyItemPosition(int position) {
        return !mIsRepeatable && (position < 0 || position >= mAdapterItemCount);
    }

    private void setSelectedPosition(int position) {
        if (mRawSelectedPosition == position) return;

        mRawSelectedPosition = position;

        if (mOnItemSelectListener != null && !isEmptyItemPosition(position)) {
            int adapterPos = getSelectedPosition();
            mOnItemSelectListener.onWheelItemSelected(this, getWheelItemDrawable(adapterPos), adapterPos);
        }
    }

    /**
     * @param position of the item in the Adapter
     * @return The Drawable at the specific position in the Adapter
     */
    public Drawable getWheelItemDrawable(int position) {
        if (mAdapter == null || mAdapterItemCount == 0) return null;

        CacheItem cacheItem = getCacheItem(position);
        if (!cacheItem.mDirty) return cacheItem.mDrawable;

        return cacheItem.mDrawable = mAdapter.getDrawable(position);
    }

    /**
     * Invalidate the drawable at the specific position so that the next Draw call
     * will refresh the Drawable at this given position in the adapter.
     */
    public void invalidateWheelItemDrawable(int position) {
        int adapterPos = rawPositionToAdapterPosition(position);
        if (isEmptyItemPosition(adapterPos)) return;

        CacheItem cacheItem = mItemCacheArray[adapterPos];
        if (cacheItem != null) cacheItem.mDirty = true;
        invalidate();
    }

    /**
     * Invalidate all wheel items. Note - If you need to change the number of items
     * in the adapter then you will need to use {@link #setAdapter}
     *
     * @see #invalidateWheelItemDrawable
     */
    public void invalidateWheelItemDrawables() {
        for (int i = 0; i < mAdapterItemCount; i++) {
            invalidateWheelItemDrawable(i);
        }
    }

    private Drawable createOvalDrawable(int color) {
        ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
        shapeDrawable.getPaint().setColor(color);
        return shapeDrawable;
    }

    /**
     * @return the adapter position that is closest to the selection
     *
     * @see #getRawSelectedPosition()
     * @see #getAngle()
     */
    public int getSelectedPosition() {
        return rawPositionToAdapterPosition(mRawSelectedPosition);
    }

    /**
     * @return the wheel angle in degrees.
     *
     * @see #getRawSelectedPosition()
     * @see #getSelectedPosition()
     */
    public float getAngle() {
        return mAngle;
    }

    private void addAngle(float degrees) {
        setAngle(mAngle + degrees);
    }

    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();

        if (!mWheelBounds.contains(x, y)) {
            if (mIsDraggingWheel) {
                flingWheel();
            }
            return true;
        }

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                if (!mIsDraggingWheel) {
                    startWheelDrag(event, x, y);
                }

                mClickedItem = getClickedItem(x, y);
                break;
            case MotionEvent.ACTION_UP:
                if (mOnItemClickListener != null && mClickedItem != null
                        && mClickedItem == getClickedItem(x, y)
                        && mDraggedAngle < CLICK_MAX_DRAGGED_ANGLE) {
                    boolean isSelected = Math.abs(mClickedItem.mRelativePos) < 1f;
                    mOnItemClickListener.onWheelItemClick(this,
                            mClickedItem.mAdapterPosition, isSelected);
                }
            case MotionEvent.ACTION_CANCEL:
                if (mIsDraggingWheel) {
                    flingWheel();
                }

                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mIsDraggingWheel) {
                    startWheelDrag(event, x, y);
                    return true;
                }

                mVelocityTracker.addMovement(event);
                mLastWheelTouchX = x;
                mLastWheelTouchY = y;
                setRadiusVector(x, y);

                float wheelRadiusSquared = mWheelBounds.getRadius() * mWheelBounds.getRadius();
                float touchRadiusSquared = mRadiusVector.x * mRadiusVector.x + mRadiusVector.y * mRadiusVector.y;
                float touchFactor = TOUCH_FACTORS[(int) (touchRadiusSquared / wheelRadiusSquared * TOUCH_FACTORS.length)];
                float touchAngle = mWheelBounds.angleToDegrees(x, y);
                float draggedAngle = -1f * Circle.shortestAngle(touchAngle, mLastTouchAngle) * touchFactor;
                addAngle(draggedAngle);
                mLastTouchAngle = touchAngle;
                mDraggedAngle += draggedAngle;

                if (mRequiresUpdate) {
                    mRequiresUpdate = false;
                }
                break;
        }
        return true;
    }

    private void startWheelDrag(MotionEvent event, float x, float y) {
        mIsDraggingWheel = true;
        mDraggedAngle = 0f;

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        } else {
            mVelocityTracker.clear();
        }
        mVelocityTracker.addMovement(event);

        mAngularVelocity = 0f;
        mLastTouchAngle = mWheelBounds.angleToDegrees(x, y);
    }

    private void flingWheel() {
        mIsDraggingWheel = false;

        mVelocityTracker.computeCurrentVelocity(1);

        //torque = r X F
        mForceVector.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
        setRadiusVector(mLastWheelTouchX, mLastWheelTouchY);
        float torque = mForceVector.crossProduct(mRadiusVector);

        //dw/dt = torque / I = torque / mr^2
        float wheelRadiusSquared = mWheelBounds.getRadius() * mWheelBounds.getRadius();
        float angularAccel = torque / wheelRadiusSquared;

        //estimate an angular velocity based on the strength of the angular acceleration
        float angularVel = angularAccel * ANGULAR_VEL_COEFFICIENT;

        //clamp the angular velocity
        if (angularVel > MAX_ANGULAR_VEL) angularVel = MAX_ANGULAR_VEL;
        else if (angularVel < -MAX_ANGULAR_VEL) angularVel = -MAX_ANGULAR_VEL;
        mAngularVelocity = angularVel;

        mLastUpdateTime = SystemClock.uptimeMillis();
        mRequiresUpdate = true;

        invalidate();
    }

    private void setRadiusVector(float x, float y) {
        float rVectorX = mWheelBounds.mCenterX - x;
        float rVectorY = mWheelBounds.mCenterY - y;
        mRadiusVector.set(rVectorX, rVectorY);
    }

    /**
     * Converts the raw position to a position within the adapter bounds.
     *
     * @see #rawPositionToWheelPosition(int, int)
     * @see #rawPositionToWheelPosition(int)
     */
    public int rawPositionToAdapterPosition(int position) {
        return mIsRepeatable ? Circle.clamp(position, mAdapterItemCount) : position;
    }

    /**
     * Converts the raw position to a position within the wheel item bounds.
     *
     * @see #rawPositionToAdapterPosition(int)
     * @see #rawPositionToWheelPosition(int, int)
     */
    public int rawPositionToWheelPosition(int position) {
        return rawPositionToWheelPosition(position, rawPositionToAdapterPosition(position));
    }

    /**
     * Converts the raw position to a position within the wheel item bounds.
     *
     * @see #rawPositionToAdapterPosition(int)
     * @see #rawPositionToWheelPosition(int)
     */
    public int rawPositionToWheelPosition(int position, int adapterPosition) {
        int circularOffset = mIsRepeatable ? ((int) Math.floor((position /
                (float) mAdapterItemCount)) * (mAdapterItemCount - mItemCount)) : 0;
        return Circle.clamp(adapterPosition + circularOffset, mItemCount);
    }

    /**
     * Estimates the wheel's new angle and angular velocity
     */
    private void update(float deltaTime) {
        float vel = mAngularVelocity;
        float velSqr = vel*vel;
        if (vel > 0f) {
            //TODO the damping is not based on time
            mAngularVelocity -= velSqr * VELOCITY_FRICTION_COEFFICIENT + CONSTANT_FRICTION_COEFFICIENT;
            if (mAngularVelocity < 0f) mAngularVelocity = 0f;
        } else if (vel < 0f) {
            mAngularVelocity -= velSqr * -VELOCITY_FRICTION_COEFFICIENT - CONSTANT_FRICTION_COEFFICIENT;
            if (mAngularVelocity > 0f) mAngularVelocity = 0f;
        }

        if (mAngularVelocity != 0f) {
            addAngle(mAngularVelocity * deltaTime);
        } else {
            mRequiresUpdate = false;
        }
    }

    private void updateWheelStateIfReq() {
        if (!mRequiresUpdate) return;

        long currentTime = SystemClock.uptimeMillis();
        long timeDiff = currentTime - mLastUpdateTime;
        mLastUpdateTime = currentTime;
        update(timeDiff);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        updateWheelStateIfReq();

        if (mWheelBounds == null) return; //issue with layoutWheel not being called before draw call

        if (mWheelDrawable != null) {
            drawWheel(canvas);
        }

        if (mAdapter != null && mAdapterItemCount > 0) {
           drawWheelItems(canvas);
        }
    }

    private void drawWheel(Canvas canvas) {
        if (mIsWheelDrawableRotatable) {
            canvas.save();
            canvas.rotate(mAngle, mWheelBounds.mCenterX, mWheelBounds.mCenterY);
            mWheelDrawable.draw(canvas);
            canvas.restore();
        } else {
            mWheelDrawable.draw(canvas);
        }
    }

    private void drawWheelItems(Canvas canvas) {
        double angleInRadians = Math.toRadians(mAngle);
        double cosAngle = Math.cos(angleInRadians);
        double sinAngle = Math.sin(angleInRadians);
        float centerX = mWheelBounds.mCenterX;
        float centerY = mWheelBounds.mCenterY;

        int wheelItemOffset = mItemCount / 2;
        int offset = mRawSelectedPosition - wheelItemOffset;
        int length = mItemCount + offset;
        for (int i = offset; i < length; i++) {
            int adapterPosition = rawPositionToAdapterPosition(i);
            int wheelItemPosition = rawPositionToWheelPosition(i, adapterPosition);

            Circle itemBounds = mWheelItemBounds.get(wheelItemPosition);
            float radius = itemBounds.mRadius;

            //translate before rotating so that origin is at the wheel's center
            float x = itemBounds.mCenterX - centerX;
            float y = itemBounds.mCenterY - centerY;

            //rotate
            float x1 = (float) (x * cosAngle - y * sinAngle);
            float y1 = (float) (x * sinAngle + y * cosAngle);

            //translate back after rotation
            x1 += centerX;
            y1 += centerY;

            ItemState itemState = mItemStates.get(wheelItemPosition);
            updateItemState(itemState, adapterPosition, x1, y1, radius);
            mItemTransformer.transform(itemState, sTempRect);

            //Empty positions can only occur from having "non repeatable" items
            CacheItem cacheItem = getCacheItem(adapterPosition);

            //don't draw if outside of the view bounds
            if (Rect.intersects(sTempRect, mViewBounds)) {
                if (cacheItem.mDirty && !cacheItem.mIsEmpty) {
                    cacheItem.mDrawable = mAdapter.getDrawable(adapterPosition);
                    cacheItem.mDirty = false;
                }

                if (!cacheItem.mIsVisible) {
                    cacheItem.mIsVisible = true;
                    if (mOnItemVisibilityChangeListener != null) {
                        mOnItemVisibilityChangeListener.onItemVisibilityChange(mAdapter, adapterPosition, true);
                    }
                }

                if (i == mRawSelectedPosition && mSelectionDrawable != null && !isEmptyItemPosition(i)) {
                    mSelectionDrawable.setBounds(sTempRect.left - mSelectionPadding, sTempRect.top - mSelectionPadding,
                            sTempRect.right + mSelectionPadding, sTempRect.bottom + mSelectionPadding);
                    mSelectionTransformer.transform(mSelectionDrawable, itemState);
                    mSelectionDrawable.draw(canvas);
                }

                Drawable drawable = cacheItem.mDrawable;
                if (drawable != null) {
                    drawable.setBounds(sTempRect);
                    drawable.draw(canvas);
                }
            } else {
                if (cacheItem != null && cacheItem.mIsVisible) {
                    cacheItem.mIsVisible = false;
                    if (mOnItemVisibilityChangeListener != null) {
                        mOnItemVisibilityChangeListener.onItemVisibilityChange(mAdapter, adapterPosition, false);
                    }
                }
            }
        }
    }

    /**
     * The ItemState is used to provide extra information when transforming the selection drawable
     * or item bounds.
     */
    public static class ItemState {
        WheelView mWheelView;
        Circle mBounds;
        float mAngleFromSelection;
        float mRelativePos;
        int mAdapterPosition; //TODO

        private ItemState() {
            mBounds = new Circle();
        }

        public WheelView getWheelView() {
            return mWheelView;
        }

        public float getAngleFromSelection() {
            return mAngleFromSelection;
        }

        public Circle getBounds() {
            return mBounds;
        }

        public float getRelativePosition() {
            return mRelativePos;
        }
    }

    private void updateItemState(ItemState itemState, int adapterPosition,
                                 float x, float y, float radius) {
        float itemAngle = mWheelBounds.angleToDegrees(x, y);
        float angleFromSelection = Circle.shortestAngle(itemAngle, mSelectionAngle);
        float relativePos = angleFromSelection / mItemAngle * 2f;

        itemState.mAngleFromSelection = angleFromSelection;
        itemState.mRelativePos = relativePos;
        itemState.mBounds.mCenterX = x;
        itemState.mBounds.mCenterY = y;
        itemState.mAdapterPosition = adapterPosition;

        //TODO The radius is always known - doesn't really need this?
        itemState.mBounds.mRadius = radius;
    }

    private ItemState getClickedItem(float touchX, float touchY) {
        for (ItemState state : mItemStates) {
            Circle itemBounds = state.mBounds;
            if (itemBounds.contains(touchX, touchY)) return state;
        }
        return null;
    }

    static class CacheItem {
        boolean mDirty;
        boolean mIsVisible;
        boolean mIsEmpty;
        Drawable mDrawable;

        CacheItem() {
            mDirty = true;
        }

        CacheItem(boolean isEmpty) {
            this();
            mIsEmpty = isEmpty;
        }
    }

    private CacheItem getCacheItem(int position) {
        if (isEmptyItemPosition(position)) return EMPTY_CACHE_ITEM;

        CacheItem cacheItem = mItemCacheArray[position];
        if (cacheItem == null) {
            cacheItem = new CacheItem();
            mItemCacheArray[position] = cacheItem;
        }
        return cacheItem;
    }

    /**
     * A simple class to represent a vector with an add and cross product method. Used only to
     * calculate the Wheel's angular velocity in {@link #flingWheel()}
     */
    static class Vector {
        float x, y;

        Vector() {}

        void set(float x, float y) {
            this.x = x;
            this.y = y;
        }

        float crossProduct(Vector vector) {
            return this.x * vector.y - this.y * vector.x;
        }

        @Override
        public String toString() {
            return "Vector: (" + this.x + ", " + this.y + ")";
        }
    }
}


package com.lukedeighton.wheelsample;

import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.lukedeighton.wheelview.WheelView;
import com.lukedeighton.wheelview.adapter.WheelArrayAdapter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class MainActivity extends Activity {

    private static final int ITEM_COUNT = 20;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final WheelView wheelView = (WheelView) findViewById(R.id.wheelview);

        //create data for the adapter
        List<Map.Entry<String, Integer>> entries = new ArrayList<Map.Entry<String, Integer>>(ITEM_COUNT);
        for (int i = 0; i < ITEM_COUNT; i++) {
            Map.Entry<String, Integer> entry = MaterialColor.random(this, "\\D*_500$");
            entries.add(entry);
        }

        //populate the adapter, that knows how to draw each item (as you would do with a ListAdapter)
        wheelView.setAdapter(new MaterialColorAdapter(entries));

        //a listener for receiving a callback for when the item closest to the selection angle changes
        wheelView.setOnWheelItemSelectedListener(new WheelView.OnWheelItemSelectListener() {
            @Override
            public void onWheelItemSelected(WheelView parent, Drawable itemDrawable, int position) {
                //get the item at this position
                Map.Entry<String, Integer> selectedEntry = ((MaterialColorAdapter) parent.getAdapter()).getItem(position);
                parent.setSelectionColor(getContrastColor(selectedEntry));
            }
        });

        wheelView.setOnWheelItemClickListener(new WheelView.OnWheelItemClickListener() {
            @Override
            public void onWheelItemClick(WheelView parent, int position, boolean isSelected) {
                String msg = String.valueOf(position) + " " + isSelected;
                Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
            }
        });

        //initialise the selection drawable with the first contrast color
        wheelView.setSelectionColor(getContrastColor(entries.get(0)));

        /*
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //wheelView.setSelectionAngle(-wheelView.getAngleForPosition(5));
                wheelView.setMidSelected();
            }
        }, 3000); */
    }

    //get the materials darker contrast
    private int getContrastColor(Map.Entry<String, Integer> entry) {
        String colorName = MaterialColor.getColorName(entry);
        return MaterialColor.getContrastColor(colorName);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    static class MaterialColorAdapter extends WheelArrayAdapter<Map.Entry<String, Integer>> {
        MaterialColorAdapter(List<Map.Entry<String, Integer>> entries) {
            super(entries);
        }

        @Override
        public Drawable getDrawable(int position) {
            Drawable[] drawable = new Drawable[] {
                    createOvalDrawable(getItem(position).getValue()),
                    new TextDrawable(String.valueOf(position))
            };
            return new LayerDrawable(drawable);
        }

        private Drawable createOvalDrawable(int color) {
            ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
            shapeDrawable.getPaint().setColor(color);
            return shapeDrawable;
        }
    }
}


package com.lukedeighton.wheelview;

import android.graphics.Rect;

public class Circle {
    float mCenterX, mCenterY;
    float mRadius;

    Circle() {}

    Circle(float centerX, float centerY, float radius) {
        mCenterX = centerX;
        mCenterY = centerY;
        mRadius = radius;
    }

    boolean contains(float x, float y) {
        x = mCenterX - x;
        y = mCenterY - y;
        return x * x + y * y <= mRadius * mRadius;
    }

    public float getCenterX() {
        return mCenterX;
    }

    public float getCenterY() {
        return mCenterY;
    }

    public float getRadius() {
        return mRadius;
    }

    Rect getBoundingRect() {
        return new Rect(Math.round(mCenterX - mRadius), Math.round(mCenterY - mRadius),
                Math.round(mCenterX + mRadius), Math.round(mCenterY + mRadius));
    }

    /**
     * The Angle from this circle's center to the position x, y
     * y is considered to go down (like android view system)
     */
    float angleTo(float x, float y) {
        return (float) Math.atan2((mCenterY - y), (x - mCenterX));
    }

    float angleToDegrees(float x, float y) {
        return (float) Math.toDegrees(angleTo(x, y));
    }

    /**
     * Clamps the value to a number between 0 and the upperLimit
     */
    static int clamp(int value, int upperLimit) {
        if (value < 0) {
            return value + (-1 * (int) Math.floor(value / (float) upperLimit)) * upperLimit;
        } else {
            return value % upperLimit;
        }
    }

    static float clamp180(float value) {
        //TODO clamp(int value, int upperLimit) could use this code? + test it
        return (((value + 180f) % 360f + 360f) % 360f) - 180f;
    }

    /**
     * Returns the shortest angle difference when the inputs range between -180 and 180 (such as from Math.atan2)
     */
    static float shortestAngle(float angleA, float angleB) {
        float angle = angleA - angleB;
        if (angle > 180f) {
            angle -=360f;
        } else if (angle < -180f) {
            angle += 360f;
        }
        return angle;
    }

    @Override
    public String toString() {
        return "Radius: " + mRadius + " X: " + mCenterX +  " Y: " + mCenterY;
    }
}


package org.eluder.coveralls.maven.plugin.util;

/*
 * #[license]
 * coveralls-maven-plugin
 * %%
 * Copyright (C) 2013 - 2016 Tapio Rautonen
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * %[license]
 */

/**
 * Wildcard text utilities.
 */
public final class Wildcards {

    /**
     * Matches text against a wildcard pattern where ? is single letter and * is zero or more
     * letters.
     *
     * @param text the text to test against
     * @param wildcard the wildcard pattern
     * @return true if the given text matches the wildcard, otherwise false
     */
    public static boolean matches(final String text, final String wildcard) {
        String pattern = wildcard.replace("?", "\\w").replace("*", "\\w*");
        return (text != null && text.matches(pattern));
    }

    private Wildcards() {
        // hide constructor
    }
}


package org.eluder.coveralls.maven.plugin;

/*
 * #[license]
 * coveralls-maven-plugin
 * %%
 * Copyright (C) 2013 - 2016 Tapio Rautonen
 * %%
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 * %[license]
 */

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.eluder.coveralls.maven.plugin.domain.CoverallsResponse;
import org.eluder.coveralls.maven.plugin.domain.Git;
import org.eluder.coveralls.maven.plugin.domain.GitRepository;
import org.eluder.coveralls.maven.plugin.domain.Job;
import org.eluder.coveralls.maven.plugin.httpclient.CoverallsClient;
import org.eluder.coveralls.maven.plugin.httpclient.CoverallsProxyClient;
import org.eluder.coveralls.maven.plugin.json.JsonWriter;
import org.eluder.coveralls.maven.plugin.logging.CoverageTracingLogger;
import org.eluder.coveralls.maven.plugin.logging.DryRunLogger;
import org.eluder.coveralls.maven.plugin.logging.JobLogger;
import org.eluder.coveralls.maven.plugin.logging.Logger;
import org.eluder.coveralls.maven.plugin.logging.Logger.Position;
import org.eluder.coveralls.maven.plugin.service.Appveyor;
import org.eluder.coveralls.maven.plugin.service.Bamboo;
import org.eluder.coveralls.maven.plugin.service.Circle;
import org.eluder.coveralls.maven.plugin.service.General;
import org.eluder.coveralls.maven.plugin.service.Jenkins;
import org.eluder.coveralls.maven.plugin.service.ServiceSetup;
import org.eluder.coveralls.maven.plugin.service.Shippable;
import org.eluder.coveralls.maven.plugin.service.Travis;
import org.eluder.coveralls.maven.plugin.service.Wercker;
import org.eluder.coveralls.maven.plugin.source.SourceCallback;
import org.eluder.coveralls.maven.plugin.source.SourceLoader;
import org.eluder.coveralls.maven.plugin.source.UniqueSourceCallback;
import org.eluder.coveralls.maven.plugin.util.CoverageParsersFactory;
import org.eluder.coveralls.maven.plugin.util.SourceLoaderFactory;
import org.eluder.coveralls.maven.plugin.util.TimestampParser;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;

@Mojo(name = "report", threadSafe = false, aggregator = true)
public class CoverallsReportMojo extends AbstractMojo {

    /**
     * File paths to additional JaCoCo coverage report files.
     */
    @Parameter(property = "jacocoReports")
    protected List<File> jacocoReports;

    /**
     * File paths to additional Cobertura coverage report files.
     */
    @Parameter(property = "coberturaReports")
    protected List<File> coberturaReports;

    /**
     * File paths to additional Saga coverage report files.
     */
    @Parameter(property = "sagaReports")
    protected List<File> sagaReports;

    /**
     * Directories for relative per module specific report files.
     */
    @Parameter(property = "relativeReportDirs")
    protected List<String> relativeReportDirs;

    /**
     * File path to write and submit Coveralls data.
     */
    @Parameter(property = "coverallsFile", defaultValue = "${project.build.directory}/coveralls.json")
    protected File coverallsFile;

    /**
     * Url for the Coveralls API.
     */
    @Parameter(property = "coverallsUrl", defaultValue = "https://coveralls.io/api/v1/jobs")
    protected String coverallsUrl;

    /**
     * Source directories.
     */
    @Parameter(property = "sourceDirectories")
    protected List<File> sourceDirectories;

    /**
     * Source file encoding.
     */
    @Parameter(property = "sourceEncoding", defaultValue = "${project.build.sourceEncoding}")
    protected String sourceEncoding;

    /**
     * CI service name.
     */
    @Parameter(property = "serviceName")
    protected String serviceName;

    /**
     * CI service job id.
     */
    @Parameter(property = "serviceJobId")
    protected String serviceJobId;

    /**
     * CI service build number.
     */
    @Parameter(property = "serviceBuildNumber")
    protected String serviceBuildNumber;

    /**
     * CI service build url.
     */
    @Parameter(property = "serviceBuildUrl")
    protected String serviceBuildUrl;

    /**
     * CI service specific environment properties.
     */
    @Parameter(property = "serviceEnvironment")
    protected Properties serviceEnvironment;

    /**
     * Coveralls repository token.
     */
    @Parameter(property = "repoToken")
    protected String repoToken;

    /**
     * Git branch name.
     */
    @Parameter(property = "branch")
    protected String branch;

    /**
     * GitHub pull request identifier.
     */
    @Parameter(property = "pullRequest")
    protected String pullRequest;

    /**
     * Build timestamp format. Must be in format supported by SimpleDateFormat.
     */
    @Parameter(property = "timestampFormat", defaultValue = "${maven.build.timestamp.format}")
    protected String timestampFormat;

    /**
     * Build timestamp. Must be in format defined by 'timestampFormat' if it's available or in
     * default timestamp format yyyy-MM-dd'T'HH:mm:ss'Z'.
     */
    @Parameter(property = "timestamp", defaultValue = "${maven.build.timestamp}")
    protected String timestamp;

    /**
     * Dry run Coveralls report without actually sending it.
     */
    @Parameter(property = "dryRun", defaultValue = "false")
    protected boolean dryRun;

    /**
     * Fail build if Coveralls service is not available or submission fails for internal errors.
     */
    @Parameter(property = "failOnServiceError", defaultValue = "true")
    protected boolean failOnServiceError;

    /**
     * Scan subdirectories for source files.
     */
    @Parameter(property = "scanForSources", defaultValue = "false")
    protected boolean scanForSources;

    /**
     * Base directory of the project.
     */
    @Parameter(property = "coveralls.basedir", defaultValue = "${project.basedir}")
    protected File basedir;

    /**
     * Skip the plugin execution.
     */
    @Parameter(property = "coveralls.skip", defaultValue = "false")
    protected boolean skip;

    /**
     * Maven settings.
     */
    @Parameter(defaultValue = "${settings}", readonly = true, required = true)
    protected Settings settings;

    /**
     * Maven project for runtime value resolution.
     */
    @Component
    protected MavenProject project;

    @Override
    public final void execute() throws MojoExecutionException, MojoFailureException {
        if (skip) {
            getLog().info("Skip property set, skipping plugin execution");
            return;
        }

        try {
            createEnvironment().setup();
            Job job = createJob();
            job.validate().throwOrInform(getLog());
            SourceLoader sourceLoader = createSourceLoader(job);
            List<CoverageParser> parsers = createCoverageParsers(sourceLoader);
            JsonWriter writer = createJsonWriter(job);
            CoverallsClient client = createCoverallsClient();
            List<Logger> reporters = new ArrayList<>();
            reporters.add(new JobLogger(job));
            SourceCallback sourceCallback = createSourceCallbackChain(writer, reporters);
            reporters.add(new DryRunLogger(job.isDryRun(), writer.getCoverallsFile()));

            report(reporters, Position.BEFORE);
            writeCoveralls(writer, sourceCallback, parsers);
            report(reporters, Position.AFTER);

            if (!job.isDryRun()) {
                submitData(client, writer.getCoverallsFile());
            }
        } catch (ProcessingException ex) {
            throw new MojoFailureException("Processing of input or output data failed", ex);
        } catch (IOException ex) {
            throw new MojoFailureException("I/O operation failed", ex);
        } catch (Exception ex) {
            throw new MojoExecutionException("Build error", ex);
        }
    }

    /**
     *
     * @param sourceLoader source loader that extracts source files
     * @return coverage parsers for all maven modules and additional reports
     * @throws IOException if parsers cannot be created
     */
    protected List<CoverageParser> createCoverageParsers(final SourceLoader sourceLoader) throws IOException {
        return new CoverageParsersFactory(project, sourceLoader)
                .withJaCoCoReports(jacocoReports)
                .withCoberturaReports(coberturaReports)
                .withSagaReports(sagaReports)
                .withRelativeReportDirs(relativeReportDirs)
                .createParsers();
    }

    /**
     * @return source loader that extracts source files
     *
     * @param job the job describing the coveralls report
     */
    protected SourceLoader createSourceLoader(final Job job) {
        return new SourceLoaderFactory(job.getGit().getBaseDir(), project, sourceEncoding)
                .withSourceDirectories(sourceDirectories)
                .withScanForSources(scanForSources)
                .createSourceLoader();
    }

    /**
     * @return environment to setup mojo and service specific properties
     */
    protected Environment createEnvironment() {
        return new Environment(this, getServices());
    }

    /**
     * @return list of available continuous integration services
     */
    protected List<ServiceSetup> getServices() {
        Map<String, String> env = System.getenv();
        List<ServiceSetup> services = new ArrayList<>();
        services.add(new Shippable(env));
        services.add(new Travis(env));
        services.add(new Circle(env));
        services.add(new Jenkins(env));
        services.add(new Bamboo(env));
        services.add(new Appveyor(env));
        services.add(new Wercker(env));
        services.add(new General(env));
        return services;
    }

    /**
     * @return job that describes the coveralls report
     * @throws ProcessingException if processing of timestamp fails
     * @throws IOException if an I/O error occurs
     */
    protected Job createJob() throws ProcessingException, IOException {
        Git git = new GitRepository(basedir).load();
        Date time = new TimestampParser(timestampFormat).parse(timestamp);
        return new Job()
            .withRepoToken(repoToken)
            .withServiceName(serviceName)
            .withServiceJobId(serviceJobId)
            .withServiceBuildNumber(serviceBuildNumber)
            .withServiceBuildUrl(serviceBuildUrl)
            .withServiceEnvironment(serviceEnvironment)
            .withDryRun(dryRun)
            .withBranch(branch)
            .withPullRequest(pullRequest)
            .withTimestamp(time)
            .withGit(git);
    }

    /**
     * @param job the job describing the coveralls report
     * @return JSON writer that writes the coveralls data
     * @throws IOException if an I/O error occurs
     */
    protected JsonWriter createJsonWriter(final Job job) throws IOException {
        return new JsonWriter(job, coverallsFile);
    }

    /**
     * @return http client that submits the coveralls data
     */
    protected CoverallsClient createCoverallsClient() {
        return new CoverallsProxyClient(coverallsUrl, settings.getActiveProxy());
    }

    /**
     * @param writer the JSON writer
     * @param reporters the logging reporters
     * @return source callback chain for different source handlers
     */
    protected SourceCallback createSourceCallbackChain(final JsonWriter writer, final List<Logger> reporters) {
        SourceCallback chain = writer;
        if (getLog().isInfoEnabled()) {
            CoverageTracingLogger coverageTracingReporter = new CoverageTracingLogger(chain);
            chain = coverageTracingReporter;
            reporters.add(coverageTracingReporter);
        }
        chain = new UniqueSourceCallback(chain);
        return chain;
    }

    /**
     * Writes coverage data to JSON file.
     *
     * @param writer JSON writer that writes the coveralls data
     * @param sourceCallback the source callback handler
     * @param parsers list of coverage parsers
     * @throws ProcessingException if process to to create JSON file fails
     * @throws IOException if an I/O error occurs
     */
    protected void writeCoveralls(final JsonWriter writer, final SourceCallback sourceCallback, final List<CoverageParser> parsers) throws ProcessingException, IOException {
        try {
            getLog().info("Writing Coveralls data to " + writer.getCoverallsFile().getAbsolutePath() + "...");
            long now = System.currentTimeMillis();
            sourceCallback.onBegin();
            for (CoverageParser parser : parsers) {
                getLog().info("Processing coverage report from " + parser.getCoverageFile().getAbsolutePath());
                parser.parse(sourceCallback);
            }
            sourceCallback.onComplete();
            long duration = System.currentTimeMillis() - now;
            getLog().info("Successfully wrote Coveralls data in " + duration + "ms");
        } finally {
            writer.close();
        }
    }

    private void submitData(final CoverallsClient client, final File coverallsFile) throws ProcessingException, IOException {
        getLog().info("Submitting Coveralls data to API");
        long now = System.currentTimeMillis();
        try {
            CoverallsResponse response = client.submit(coverallsFile);
            long duration = System.currentTimeMillis() - now;
            getLog().info("Successfully submitted Coveralls data in " + duration + "ms for " + response.getMessage());
            getLog().info(response.getUrl());
            getLog().info("*** It might take hours for Coveralls to update the actual coverage numbers for a job");
            getLog().info("    If you see question marks in the report, please be patient");
        } catch (ProcessingException ex) {
            long duration = System.currentTimeMillis() - now;
            String message = "Submission failed in " + duration + "ms while processing data";
            handleSubmissionError(ex, message, true);
        } catch (IOException ex) {
            long duration = System.currentTimeMillis() - now;
            String message = "Submission failed in " + duration + "ms while handling I/O operations";
            handleSubmissionError(ex, message, failOnServiceError);
        }
    }

    private <T extends Exception> void handleSubmissionError(final T ex, final String message, final boolean failOnException) throws T {
        if (failOnException) {
            getLog().error(message);
            throw ex;
        } else {
            getLog().warn(message);
        }
    }

    private void report(final List<Logger> reporters, final Position position) {
        for (Logger reporter : reporters) {
            if (position.equals(reporter.getPosition())) {
                reporter.log(getLog());
            }
        }
    }
}


/*
 * Copyright (c) 1998-2018 University Corporation for Atmospheric Research/Unidata
 * See LICENSE for license information.
 */
package ucar.ma2;

import java.nio.ByteBuffer;
import java.util.Iterator;

/**
 * Concrete implementation of Array specialized for chars.
 * Data storage is with 1D java array of chars.
 *

 * issues: what should we do if a conversion loses accuracy? nothing ? Exception ?
 *
 * @author caron
 * @see Array
 */
public class ArrayChar extends Array implements Iterable<String> {

  /**
   * package private. use Array.factory()
   */
  static ArrayChar factory(Index index) {
    return ArrayChar.factory(index, null);
  }

  /* create new ArrayChar with given indexImpl and backing store.
   * Should be private.
   * @param index use this Index
   * @param stor. use this storage. if null, allocate.
   * @return. new ArrayDouble.D<rank> or ArrayDouble object.
   */
  static ArrayChar factory(Index index, char[] storage) {
    if (index instanceof Index0D) {
      return new ArrayChar.D0(index, storage);
    } else if (index instanceof Index1D) {
      return new ArrayChar.D1(index, storage);
    } else if (index instanceof Index2D) {
      return new ArrayChar.D2(index, storage);
    } else if (index instanceof Index3D) {
      return new ArrayChar.D3(index, storage);
    } else if (index instanceof Index4D) {
      return new ArrayChar.D4(index, storage);
    } else if (index instanceof Index5D) {
      return new ArrayChar.D5(index, storage);
    } else if (index instanceof Index6D) {
      return new ArrayChar.D6(index, storage);
    } else if (index instanceof Index7D) {
      return new ArrayChar.D7(index, storage);
    } else {
      return new ArrayChar(index, storage);
    }
  }

  ///////////////////////////////////////////////////////////////////////////////
  protected char[] storage;

  /**
   * Create a new Array of type char and the given shape.
   * dimensions.length determines the rank of the new Array.
   *
   * @param dimensions the shape of the Array.
   */
  public ArrayChar(int[] dimensions) {
    super(DataType.CHAR, dimensions);
    storage = new char[(int) indexCalc.getSize()];
  }

  /**
   * Create a new Array using the given IndexArray and backing store.
   * used for sections. Trusted package private.
   *
   * @param ima  use this IndexArray as the index
   * @param data use this as the backing store
   */
  ArrayChar(Index ima, char[] data) {
    super(DataType.CHAR, ima);
    /* replace by something better
    if (ima.getSize() != data.length)
      throw new IllegalArgumentException("bad data length"); */
    if (data != null)
      storage = data;
    else
      storage = new char[(int) ima.getSize()];
  }

  public ArrayChar(String s) {
    super(DataType.CHAR, new int[] {s.length()});
    storage = new char[s.length()];
    for (int i=0; i<s.length(); i++)
      storage[i] = s.charAt(i);
  }

  /**
   * create new Array with given indexImpl and same backing store
   */
  protected Array createView(Index index) {
    return ArrayChar.factory(index, storage);
  }

  // used only by copyTo1DJavaArray
  public Object getStorage() {
    return storage;
  }

  // copy from javaArray to storage using the iterator: used by factory( Object);
  protected void copyFrom1DJavaArray(IndexIterator iter, Object javaArray) {
    char[] ja = (char[]) javaArray;
    for (char aJa : ja) iter.setCharNext(aJa);
  }

  // copy to javaArray from storage using the iterator: used by copyToNDJavaArray;
  protected void copyTo1DJavaArray(IndexIterator iter, Object javaArray) {
    char[] ja = (char[]) javaArray;
    for (int i = 0; i < ja.length; i++)
      ja[i] = iter.getCharNext();
  }

  /**
   * Trasfer data to a ByteBuffer.
   * Note we cast char to byte, discarding top byte, if any.
   * This is because CDM char is really a byte, not a java char.
   * @return data in a ByteBuffer
   */
    @Override
  public ByteBuffer getDataAsByteBuffer() {
    ByteBuffer bb = ByteBuffer.allocate((int)getSize());
    resetLocalIterator();
    while (hasNext())
      bb.put( nextByte());
    return bb;
  }

  /**
   * Return the element class type
   */
  public Class getElementType() {
    return char.class;
  }

  /**
   * get the value at the specified index.
   */
  public char get(Index i) {
    return storage[i.currentElement()];
  }

  /**
   * set the value at the sepcified index.
   */
  public void set(Index i, char value) {
    storage[i.currentElement()] = value;
  }

  public double getDouble(Index i) {
    return (double) storage[i.currentElement()];
  }

  public void setDouble(Index i, double value) {
    storage[i.currentElement()] = (char) value;
  }

  public float getFloat(Index i) {
    return (float) storage[i.currentElement()];
  }

  public void setFloat(Index i, float value) {
    storage[i.currentElement()] = (char) value;
  }

  public long getLong(Index i) {
    return (long) storage[i.currentElement()];
  }

  public void setLong(Index i, long value) {
    storage[i.currentElement()] = (char) value;
  }

  public int getInt(Index i) {
    return (int) storage[i.currentElement()];
  }

  public void setInt(Index i, int value) {
    storage[i.currentElement()] = (char) value;
  }

  public short getShort(Index i) {
    return (short) storage[i.currentElement()];
  }

  public void setShort(Index i, short value) {
    storage[i.currentElement()] = (char) value;
  }

  public byte getByte(Index i) {
    return (byte) storage[i.currentElement()];
  }

  public void setByte(Index i, byte value) {
    storage[i.currentElement()] = (char) value;
  }

  public char getChar(Index i) {
    return storage[i.currentElement()];
  }

  public void setChar(Index i, char value) {
    storage[i.currentElement()] = value;
  }

  /**
   * not legal, throw ForbiddenConversionException
   */
  public boolean getBoolean(Index i) {
    throw new ForbiddenConversionException();
  }

  /**
   * not legal, throw ForbiddenConversionException
   */
  public void setBoolean(Index i, boolean value) {
    throw new ForbiddenConversionException();
  }

  public Object getObject(Index i) {
    return storage[i.currentElement()];
  }

  public void setObject(Index i, Object value) {
    storage[i.currentElement()] = (Character) value;
  }

  /**
   * Create a String out of this rank one ArrayChar object.
   * If there is a null (0) value in the ArrayChar array, the String will end there.
   * The null is not returned as part of the String.
   *
   * @return String value of CharArray
   * @throws IllegalArgumentException if rank != 1
   */
  public String getString() {
    int rank = getRank();
    if (rank == 0) {
      return new String(storage);
    }

    if (rank != 1)
      throw new IllegalArgumentException("ArayChar.getString rank must be 1");
    int strLen = indexCalc.getShape(0);

    int count = 0;
    for (int k = 0; k < strLen; k++) {
      if (0 == storage[k])
        break;
      count++;
    }
    return new String(storage, 0, count);
  }

  /**
   * Create a String out of this rank two ArrayChar object.
   * This treats the ArrayChar as a 1D array of Strings.
   * If there is a null (0) value in the ArrayChar array, the String will end there.
   * The null is not returned as part of the String.
   *
   * @param index index into 1D String array, must be < getShape(0).
   * @return String value
   * @throws IllegalArgumentException if rank != 2
   */
  public String getString(int index) {
    Index ima = getIndex();
    return getString(ima.set(index));
  }

  /**
   * Create a String out of this ArrayChar object. The rank must be 1 or greater.
   * If there is a null (0) value in the ArrayChar array, the String will end there.
   * The null is not returned as part of the String.
   *

   * If rank=1, then this will make a string out of the entire CharArray, ignoring ima.
   * If rank is greater than 1, then make a String out of the characters of the last dimension,
   * indexed by ima.  This method treats the CharArray like an array of Strings, and
   * allows you to iterate over them, eg for a 2D ArrayChar:
   *

   * ArrayChar ca;
   * Index ima = ca.getIndex();
   * for (int i=0; i<ca.getShape()[0]; i++)
   * String s = ca.getString(ima.set0(i));
   *
   *
   * @param ima the Index of where in the Array to start.
   * @return String value of CharArray
   * @throws IllegalArgumentException if rank != 1
   */
  public String getString(Index ima) {
    int rank = getRank();
    if (rank == 0)
      throw new IllegalArgumentException("ArayChar.getString rank must not be 0");
    if (rank == 1)
      return getString();

    int strLen = indexCalc.getShape(rank - 1);

    char[] carray = new char[strLen];
    int count = 0;
    for (int k = 0; k < strLen; k++) {
      ima.setDim(rank - 1, k);
      carray[k] = getChar(ima);
      if (0 == carray[k])
        break;
      count++;
    }
    return new String(carray, 0, count);
  }

  /**
   * Set the ArrayChar values from the characters in the String. Rank must be 1.
   * If String longer than ArrayChar, ignore extra chars; if shorter, fill with 0.
   *
   * @param val set characters from this String
   * @throws IllegalArgumentException if rank != 2
   */
  public void setString(String val) {
    int rank = getRank();
    if (rank != 1)
      throw new IllegalArgumentException("ArayChar.setString rank must be 1");
    int arrayLen = indexCalc.getShape(0);
    int strLen = Math.min(val.length(), arrayLen);

    for (int k = 0; k < strLen; k++)
      storage[k] = val.charAt(k);

    char c = 0;
    for (int k = strLen; k < arrayLen; k++)
      storage[k] = c;
  }

  /**
   * Set the ArrayChar values from the characters in the String. Rank must be 2.
   * This treats the ArrayChar as a 1D array of Strings.
   * If String val longer than ArrayChar, ignore extra chars; if shorter, fill with 0.
   *

   *

   * String[] val = new String[n];
   * ArrayChar ca;
   * Index ima = ca.getIndex();
   * for (int i=0; i<n; i++)
   * ca.setString(i, val[i]);
   *
   *
   * @param index index into 1D String array, must be < getShape(0).
   * @param val   set chars from this sString
   */
  public void setString(int index, String val) {
    int rank = getRank();
    if (rank != 2)
      throw new IllegalArgumentException("ArrayChar.setString rank must be 2");

    Index ima = getIndex();
    setString(ima.set(index), val);
  }

  /**
   * Set the ArrayChar values from the characters in the String. Rank must be 1 or greater.
   * If String longer than ArrayChar, ignore extra chars; if shorter, fill with 0.
   * If rank 1, set entire ArrayChar, ignoring ima.
   * If rank > 1, treat the ArrayChar like an array of Strings of rank-1, and set the
   * row indexed by ima. For example, rank 3:
   *

   * String[][] val;
   * ArrayChar ca;
   * Index ima = ca.getIndex();
   * int rank0 = ca.getShape()[0];
   * int rank1 = ca.getShape()[1];
   *

   * for (int i=0; i<rank0; i++)
   * for (int j=0; j<rank1; j++) {
   * ima.set(i,j);
   * ca.setString(ima, val[i][j]);
   * }
   *
   *
   * @param ima the Index of where in the Array to start.
   * @param val set to this value
   */
  public void setString(Index ima, String val) {
    int rank = getRank();
    if (rank == 0)
      throw new IllegalArgumentException("ArrayChar.setString rank must not be 0");
    int arrayLen = indexCalc.getShape(rank - 1);
    int strLen = Math.min(val.length(), arrayLen);

    int count = 0;
    for (int k = 0; k < strLen; k++) {
      ima.setDim(rank - 1, k);
      setChar(ima, val.charAt(k));
      count++;
    }
    char c = 0;
    for (int k = count; k < arrayLen; k++) {
      ima.setDim(rank - 1, k);
      setChar(ima, c);
    }
  }

  // package private : mostly for iterators
  public double getDouble(int index) {
    return (double) storage[index];
  }

  public void setDouble(int index, double value) {
    storage[index] = (char) value;
  }

  public float getFloat(int index) {
    return storage[index];
  }

  public void setFloat(int index, float value) {
    storage[index] = (char) value;
  }

  public long getLong(int index) {
    return (long) storage[index];
  }

  public void setLong(int index, long value) {
    storage[index] = (char) value;
  }

  public int getInt(int index) {
    return (int) storage[index];
  }

  public void setInt(int index, int value) {
    storage[index] = (char) value;
  }

  public short getShort(int index) {
    return (short) storage[index];
  }

  public void setShort(int index, short value) {
    storage[index] = (char) value;
  }

  public byte getByte(int index) {
    return (byte) storage[index];
  }

  public void setByte(int index, byte value) {
    storage[index] = (char) (value & 0xFF);
  }

  public char getChar(int index) {
    return storage[index];
  }

  public void setChar(int index, char value) {
    storage[index] = value;
  }

  public boolean getBoolean(int index) {
    throw new ForbiddenConversionException();
  }

  public void setBoolean(int index, boolean value) {
    throw new ForbiddenConversionException();
  }

  public Object getObject(int index) {
    return getChar(index);
  }

  public void setObject(int index, Object value) {
    storage[index] = (Character) value;
  }

  /**
   * Concrete implementation of Array specialized for char, rank 0.
   */
  public static class D0 extends ArrayChar {
    private Index0D ix;

    /**
     * Constructor.
     */
    public D0() {
      super(new int[]{});
      ix = (Index0D) indexCalc;
    }

    private D0(Index i, char[] store) {
      super(i, store);
      ix = (Index0D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get() {
      return storage[ix.currentElement()];
    }

    /**
     * set the value.
     */
    public void set(char value) {
      storage[ix.currentElement()] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 1.
   */
  public static class D1 extends ArrayChar {
    private Index1D ix;

    /**
     * Constructor for array of shape {len0}.
     */
    public D1(int len0) {
      super(new int[]{len0});
      ix = (Index1D) indexCalc;
    }

    private D1(Index i, char[] store) {
      super(i, store);
      ix = (Index1D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i) {
      return storage[ix.setDirect(i)];
    }

    /**
     * set the value.
     */
    public void set(int i, char value) {
      storage[ix.setDirect(i)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 2.
   */
  public static class D2 extends ArrayChar {
    private Index2D ix;

    /**
     * Constructor for array of shape {len0,len1}.
     */
    public D2(int len0, int len1) {
      super(new int[]{len0, len1});
      ix = (Index2D) indexCalc;
    }

    private D2(Index i, char[] store) {
      super(i, store);
      ix = (Index2D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i, int j) {
      return storage[ix.setDirect(i, j)];
    }

    /**
     * set the value.
     */
    public void set(int i, int j, char value) {
      storage[ix.setDirect(i, j)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 3.
   */
  public static class D3 extends ArrayChar {
    private Index3D ix;

    /**
     * Constructor for array of shape {len0,len1,len2}.
     */
    public D3(int len0, int len1, int len2) {
      super(new int[]{len0, len1, len2});
      ix = (Index3D) indexCalc;
    }

    private D3(Index i, char[] store) {
      super(i, store);
      ix = (Index3D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i, int j, int k) {
      return storage[ix.setDirect(i, j, k)];
    }

    /**
     * set the value.
     */
    public void set(int i, int j, int k, char value) {
      storage[ix.setDirect(i, j, k)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 4.
   */
  public static class D4 extends ArrayChar {
    private Index4D ix;

    /**
     * Constructor for array of shape {len0,len1,len2,len3}.
     */
    public D4(int len0, int len1, int len2, int len3) {
      super(new int[]{len0, len1, len2, len3});
      ix = (Index4D) indexCalc;
    }

    private D4(Index i, char[] store) {
      super(i, store);
      ix = (Index4D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i, int j, int k, int l) {
      return storage[ix.setDirect(i, j, k, l)];
    }

    /**
     * set the value.
     */
    public void set(int i, int j, int k, int l, char value) {
      storage[ix.setDirect(i, j, k, l)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 5.
   */
  public static class D5 extends ArrayChar {
    private Index5D ix;

    /**
     * Constructor for array of shape {len0,len1,len2,len3,len4}.
     */
    public D5(int len0, int len1, int len2, int len3, int len4) {
      super(new int[]{len0, len1, len2, len3, len4});
      ix = (Index5D) indexCalc;
    }

    private D5(Index i, char[] store) {
      super(i, store);
      ix = (Index5D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i, int j, int k, int l, int m) {
      return storage[ix.setDirect(i, j, k, l, m)];
    }

    /**
     * set the value.
     */
    public void set(int i, int j, int k, int l, int m, char value) {
      storage[ix.setDirect(i, j, k, l, m)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 6.
   */
  public static class D6 extends ArrayChar {
    private Index6D ix;

    /**
     * Constructor for array of shape {len0,len1,len2,len3,len4,len5,}.
     */
    public D6(int len0, int len1, int len2, int len3, int len4, int len5) {
      super(new int[]{len0, len1, len2, len3, len4, len5});
      ix = (Index6D) indexCalc;
    }

    private D6(Index i, char[] store) {
      super(i, store);
      ix = (Index6D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i, int j, int k, int l, int m, int n) {
      return storage[ix.setDirect(i, j, k, l, m, n)];
    }

    /**
     * set the value.
     */
    public void set(int i, int j, int k, int l, int m, int n, char value) {
      storage[ix.setDirect(i, j, k, l, m, n)] = value;
    }
  }

  /**
   * Concrete implementation of Array specialized for char, rank 7.
   */
  public static class D7 extends ArrayChar {
    private Index7D ix;

    /**
     * Constructor for array of shape {len0,len1,len2,len3,len4,len5,len6}.
     */
    public D7(int len0, int len1, int len2, int len3, int len4, int len5, int len6) {
      super(new int[]{len0, len1, len2, len3, len4, len5, len6});
      ix = (Index7D) indexCalc;
    }

    private D7(Index i, char[] store) {
      super(i, store);
      ix = (Index7D) indexCalc;
    }

    /**
     * get the value.
     */
    public char get(int i, int j, int k, int l, int m, int n, int o) {
      return storage[ix.setDirect(i, j, k, l, m, n, o)];
    }

    /**
     * set the value.
     */
    public void set(int i, int j, int k, int l, int m, int n, int o, char value) {
      storage[ix.setDirect(i, j, k, l, m, n, o)] = value;
    }
  }

  public String toString() {
    StringBuilder sbuff = new StringBuilder();
    StringIterator ii = getStringIterator();
    int count = 0;
    while (ii.hasNext()) {
      if (count > 0) sbuff.append(",");
      String data = ii.next();
      sbuff.append(data);
      count++;
    }
    return sbuff.toString();
  }

  ///////////////////////////////////////////////////////////////////////////////
  /**
   * Treat this Variable as an array of Strings, and iterate over all the strings in the array.
   * If rank == 0, will make single String of length 1.
   *
   * @return Iterator over Strings.
   */
  public StringIterator getStringIterator() {
    return new StringIterator();
  }

  @Override
  public Iterator<String> iterator() {
    return new StringIterator();
  }

  /**
   * rank must be > 0
   */
  public class StringIterator implements Iterator<String> {
    private IndexIterator ii = getIndexIterator();
    private int strLen;
    private char[] carray;

    StringIterator() {
      if (rank == 0)
        strLen = 1;
      else
        strLen = indexCalc.getShape(rank - 1);
      carray = new char[strLen];
    }

    public int getNumElems() {
      return (int) getSize() / strLen;
    }

    public boolean hasNext() {
      return ii.hasNext();
    }

    public String next() {
      int stop = strLen;
      for (int k = 0; k < strLen; k++) {
        carray[k] = ii.getCharNext();
        if (0 == carray[k] && stop == strLen) // catch zero termination
          stop = k;
      }
      return new String(carray, 0, stop);
    }
  }

  /**
   * Make this into the equivilent 1D ArrayObject of Strings.
   *
   * @return 1D ArrayObject of Strings
   */
  public ArrayObject make1DStringArray() {
    int nelems = (getRank() == 0) ? 1 : (int) getSize() / indexCalc.getShape(getRank()-1);
    Array sarr = Array.factory(DataType.STRING, new int[]{nelems});
    IndexIterator newsiter = sarr.getIndexIterator();

    ArrayChar.StringIterator siter = getStringIterator();
    while (siter.hasNext()) {
      newsiter.setObjectNext(siter.next());
    }
    return (ArrayObject) sarr;
  }

  /**
   * Create an ArrayChar from a String
   *
   * @param s String
   * @param max maximum length
   * @return equivilent ArrayChar
   */
  public static ArrayChar makeFromString(String s, int max) {
    ArrayChar result = new ArrayChar.D1( max);
    for (int i=0; i<max && i<s.length(); i++)
      result.setChar( i, s.charAt(i));

    return result;
  }

  /**
   * Create an ArrayChar from an ArrayObject of Strings.
   *
   * @param values ArrayObject of String
   * @return equivilent ArrayChar, using maximum length of String. Unused are zero filled.
   */
  public static ArrayChar makeFromStringArray(ArrayObject values) {
    // find longest string
    IndexIterator ii = values.getIndexIterator();
    int strlen = 0;
    while (ii.hasNext()) {
      String s = (String) ii.next();
      strlen = Math.max(s.length(), strlen);
    }

    return makeFromStringArray(values, strlen);
  }

  /**
   * Create an ArrayChar from an ArrayObject of Strings.
   * Inverse of make1DStringArray. Copies the data.
   *
   * @param values ArrayObject of String
   * @param strlen string length dimension size
   * @return equivilent ArrayChar. Unused are zero filled.
   */
  public static ArrayChar makeFromStringArray(ArrayObject values, int strlen) {

    // create shape for equivilent charArray
    try {
      Section section = new Section(values.getShape());
      section.appendRange(strlen);

      int[] shape = section.getShape();
      long size = section.computeSize();

      // populate char array
      char[] cdata = new char[(int) size];
      int start = 0;
      IndexIterator ii = values.getIndexIterator();
      while (ii.hasNext()) {
        String s = (String) ii.next();
        for (int k = 0; k < s.length() && k < strlen; k++)
          cdata[start + k] = s.charAt(k);
        start += strlen;
      }

      // ready to create the char Array
      Array carr = Array.factory(DataType.CHAR, shape, cdata);
      return (ArrayChar) carr;
    } catch (InvalidRangeException e) {
      e.printStackTrace();  // cant happen.
      return null;
    }
  }

}