/*
 * Copyright (C) 2013 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 com.android.tools.perflib.vmtrace.viz;

import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

/**
 * {@link ZoomPanInteractor} listens to mouse events, interprets dragging the mouse as an attempt
 * to pan the canvas, and mouse wheel rotation as an attempt to zoom in/out the canvas.
 * After such events, it updates its listeners with the updated transformation matrix corresponding
 * to the zoom &amp; pan values.
 */
public class ZoomPanInteractor implements MouseListener, MouseMotionListener, MouseWheelListener {
    /**
     * The values from {@link java.awt.event.MouseWheelEvent#getWheelRotation()} are quite high even
     * for a small amount of scrolling. This is an arbitrary scale factor used to go from the wheel
     * rotation value to a zoom by factor. The scale is negated to take care of the common
     * expectation that scrolling down should zoom out, not zoom in.
     */
    private static final double WHEEL_UNIT_SCALE = -0.1;

    private final AffineTransform mTransform = new AffineTransform();
    private AffineTransform mInverseTransform;

    private final Point2D mTmpPoint = new Point2D.Double();

    private int mLastX;
    private int mLastY;

    private final List<ViewTransformListener> mListeners = new ArrayList<ViewTransformListener>();

    @Override
    public void mouseClicked(MouseEvent e) {
    }

    @Override
    public void mousePressed(MouseEvent e) {
        mLastX = e.getX();
        mLastY = e.getY();
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        int deltaX = e.getX() - mLastX;
        int deltaY = e.getY() - mLastY;

        translateBy(deltaX, deltaY);

        notifyTransformChange();

        mLastX = e.getX();
        mLastY = e.getY();
    }

    @VisibleForTesting
    void translateBy(int deltaX, int deltaY) {
        // Transform pixels by the current viewport scaling factor.
        // i.e when you have zoomed out by say 2x, and you drag by a pixel,
        // you expect it to move (the model space) by 2 pixels, not one.
        deltaX /= mTransform.getScaleX();
        deltaY /= mTransform.getScaleY();

        mTransform.translate(deltaX, deltaY);

        // Do not allow panning above the axis.
        // TODO: This actually encodes information about the canvas and what is drawn over here,
        // and should be moved out.
        if (mTransform.getTranslateY() > 0) {
            mTransform.translate(0, -mTransform.getTranslateY());
        }
    }

    @Override
    public void mouseReleased(MouseEvent e) {
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

    @Override
    public void mouseMoved(MouseEvent e) {
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
        if (e.getScrollType() != MouseWheelEvent.WHEEL_UNIT_SCROLL) {
            return;
        }

        double scale = 1 + WHEEL_UNIT_SCALE * e.getWheelRotation();

        // convert mouse x, y from screen coordinates to absolute coordinates
        mTmpPoint.setLocation(e.getX(), e.getY());
        mInverseTransform.transform(mTmpPoint, mTmpPoint);

        zoomBy(scale, 1, mTmpPoint);

        notifyTransformChange();
    }

    @VisibleForTesting
    void zoomBy(double scaleX, double scaleY, Point2D location) {
        // When zooming, we want to zoom by the location the mouse currently points to.
        // So we translate the current location to the origin, apply the scale, and translate back
        mTransform.translate(location.getX(), location.getY());
        mTransform.scale(scaleX, scaleY);
        mTransform.translate(-location.getX(), -location.getY());
    }

    private void notifyTransformChange() {
        try {
            mInverseTransform = mTransform.createInverse();
        } catch (NoninvertibleTransformException ignored) {
            // The transform matrix is only scaled or translated, both of which are invertible.
        }

        for (ViewTransformListener l : mListeners) {
            l.transformChanged(mTransform);
        }
    }

    public void setToScaleX(double sx, double sy) {
        mTransform.setToScale(sx, sy);
        notifyTransformChange();
    }

    @VisibleForTesting
    AffineTransform getTransform() {
        return mTransform;
    }

    public interface ViewTransformListener {
        void transformChanged(@NonNull AffineTransform transform);
    }

    public void addViewTransformListener(@NonNull ViewTransformListener l) {
        mListeners.add(l);
    }
}
