/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.gesture;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;

import java.io.IOException;
import java.io.DataOutputStream;
import java.io.DataInputStream;
import java.util.ArrayList;

/**
 * A gesture stroke started on a touch down and ended on a touch up. A stroke
 * consists of a sequence of timed points. One or multiple strokes form a gesture.
 */
public class GestureStroke {
    static final float TOUCH_TOLERANCE = 3;

    public final RectF boundingBox;

    public final float length;
    public final float[] points;

    private final long[] timestamps;
    private Path mCachedPath;

    /**
     * A constructor that constructs a gesture stroke from a list of gesture points.
     * 
     * @param points
     */
    public GestureStroke(ArrayList<GesturePoint> points) {
        final int count = points.size();
        final float[] tmpPoints = new float[count * 2];
        final long[] times = new long[count];

        RectF bx = null;
        float len = 0;
        int index = 0;

        for (int i = 0; i < count; i++) {
            final GesturePoint p = points.get(i);
            tmpPoints[i * 2] = p.x;
            tmpPoints[i * 2 + 1] = p.y;
            times[index] = p.timestamp;

            if (bx == null) {
                bx = new RectF();
                bx.top = p.y;
                bx.left = p.x;
                bx.right = p.x;
                bx.bottom = p.y;
                len = 0;
            } else {
                len += Math.hypot(p.x - tmpPoints[(i - 1) * 2], p.y - tmpPoints[(i -1) * 2 + 1]);
                bx.union(p.x, p.y);
            }
            index++;
        }
        
        timestamps = times;
        this.points = tmpPoints;
        boundingBox = bx;
        length = len;
    }

    /**
     * A faster constructor specially for cloning a stroke.
     */
    private GestureStroke(RectF bbx, float len, float[] pts, long[] times) {
        boundingBox = new RectF(bbx.left, bbx.top, bbx.right, bbx.bottom);
        length = len;
        points = pts.clone();
        timestamps = times.clone();
    }
    
    @Override
    public Object clone() {
        return new GestureStroke(boundingBox, length, points, timestamps);
    }
    
    /**
     * Draws the stroke with a given canvas and paint.
     * 
     * @param canvas
     */
    void draw(Canvas canvas, Paint paint) {
        if (mCachedPath == null) {
            makePath();
        }

        canvas.drawPath(mCachedPath, paint);
    }

    public Path getPath() {
        if (mCachedPath == null) {
            makePath();
        }

        return mCachedPath;
    }

    private void makePath() {
        final float[] localPoints = points;
        final int count = localPoints.length;

        Path path = null;

        float mX = 0;
        float mY = 0;

        for (int i = 0; i < count; i += 2) {
            float x = localPoints[i];
            float y = localPoints[i + 1];
            if (path == null) {
                path = new Path();
                path.moveTo(x, y);
                mX = x;
                mY = y;
            } else {
                float dx = Math.abs(x - mX);
                float dy = Math.abs(y - mY);
                if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
                    path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
                    mX = x;
                    mY = y;
                }
            }
        }

        mCachedPath = path;
    }

    /**
     * Converts the stroke to a Path of a given number of points.
     * 
     * @param width the width of the bounding box of the target path
     * @param height the height of the bounding box of the target path
     * @param numSample the number of points needed
     * 
     * @return the path
     */
    public Path toPath(float width, float height, int numSample) {
        final float[] pts = GestureUtils.temporalSampling(this, numSample);
        final RectF rect = boundingBox;

        GestureUtils.translate(pts, -rect.left, -rect.top);
        
        float sx = width / rect.width();
        float sy = height / rect.height();
        float scale = sx > sy ? sy : sx;
        GestureUtils.scale(pts, scale, scale);

        float mX = 0;
        float mY = 0;

        Path path = null;

        final int count = pts.length;

        for (int i = 0; i < count; i += 2) {
            float x = pts[i];
            float y = pts[i + 1];
            if (path == null) {
                path = new Path();
                path.moveTo(x, y);
                mX = x;
                mY = y;
            } else {
                float dx = Math.abs(x - mX);
                float dy = Math.abs(y - mY);
                if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
                    path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
                    mX = x;
                    mY = y;
                }
            }
        }

        return path;
    }

    void serialize(DataOutputStream out) throws IOException {
        final float[] pts = points;
        final long[] times = timestamps;
        final int count = points.length;

        // Write number of points
        out.writeInt(count / 2);

        for (int i = 0; i < count; i += 2) {
            // Write X
            out.writeFloat(pts[i]);
            // Write Y
            out.writeFloat(pts[i + 1]);
            // Write timestamp
            out.writeLong(times[i / 2]);
        }
    }

    static GestureStroke deserialize(DataInputStream in) throws IOException {
        // Number of points
        final int count = in.readInt();

        final ArrayList<GesturePoint> points = new ArrayList<GesturePoint>(count);
        for (int i = 0; i < count; i++) {
            points.add(GesturePoint.deserialize(in));
        }

        return new GestureStroke(points);
    }    

    /**
     * Invalidates the cached path that is used to render the stroke.
     */
    public void clearPath() {
        if (mCachedPath != null) mCachedPath.rewind();
    }
    
    /**
     * Computes an oriented bounding box of the stroke.
     * 
     * @return OrientedBoundingBox
     */
    public OrientedBoundingBox computeOrientedBoundingBox() {
        return GestureUtils.computeOrientedBoundingBox(points);
    }
}
