/*
 * Copyright (C) 2010 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.hierarchyviewerlib.ui;

import com.android.hierarchyviewerlib.models.PixelPerfectModel;
import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;

public class PixelPerfectLoupe extends Canvas implements IImageChangeListener {
    private PixelPerfectModel mModel;

    private Image mImage;

    private Image mGrid;

    private Color mCrosshairColor;

    private int mWidth;

    private int mHeight;

    private Point mCrosshairLocation;

    private int mZoom;

    private Transform mTransform;

    private int mCanvasWidth;

    private int mCanvasHeight;

    private Image mOverlayImage;

    private double mOverlayTransparency;

    private boolean mShowOverlay = false;

    public PixelPerfectLoupe(Composite parent) {
        super(parent, SWT.NONE);
        mModel = PixelPerfectModel.getModel();
        mModel.addImageChangeListener(this);

        addPaintListener(mPaintListener);
        addMouseListener(mMouseListener);
        addMouseWheelListener(mMouseWheelListener);
        addDisposeListener(mDisposeListener);
        addKeyListener(mKeyListener);

        mCrosshairColor = new Color(Display.getDefault(), new RGB(255, 94, 254));

        mTransform = new Transform(Display.getDefault());

        imageLoaded();
    }

    public void setShowOverlay(boolean value) {
        synchronized (this) {
            mShowOverlay = value;
        }
        doRedraw();
    }

    private DisposeListener mDisposeListener = new DisposeListener() {
        @Override
        public void widgetDisposed(DisposeEvent e) {
            mModel.removeImageChangeListener(PixelPerfectLoupe.this);
            mCrosshairColor.dispose();
            mTransform.dispose();
            if (mGrid != null) {
                mGrid.dispose();
            }
        }
    };

    private MouseListener mMouseListener = new MouseListener() {

        @Override
        public void mouseDoubleClick(MouseEvent e) {
            // pass
        }

        @Override
        public void mouseDown(MouseEvent e) {
            handleMouseEvent(e);
        }

        @Override
        public void mouseUp(MouseEvent e) {
            //
        }

    };

    private MouseWheelListener mMouseWheelListener = new MouseWheelListener() {
        @Override
        public void mouseScrolled(MouseEvent e) {
            int newZoom = -1;
            synchronized (PixelPerfectLoupe.this) {
                if (mImage != null && mCrosshairLocation != null) {
                    if (e.count > 0) {
                        newZoom = mZoom + 1;
                    } else {
                        newZoom = mZoom - 1;
                    }
                }
            }
            if (newZoom != -1) {
                mModel.setZoom(newZoom);
            }
        }
    };

    private void handleMouseEvent(MouseEvent e) {
        int newX = -1;
        int newY = -1;
        synchronized (PixelPerfectLoupe.this) {
            if (mImage == null) {
                return;
            }
            int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2;
            int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2;
            int x = (e.x - zoomedX) / mZoom;
            int y = (e.y - zoomedY) / mZoom;
            if (x >= 0 && x < mWidth && y >= 0 && y < mHeight) {
                newX = x;
                newY = y;
            }
        }
        if (newX != -1) {
            mModel.setCrosshairLocation(newX, newY);
        }
    }

    private KeyListener mKeyListener = new KeyListener() {

        @Override
        public void keyPressed(KeyEvent e) {
            boolean crosshairMoved = false;
            synchronized (PixelPerfectLoupe.this) {
                if (mImage != null) {
                    switch (e.keyCode) {
                        case SWT.ARROW_UP:
                            if (mCrosshairLocation.y != 0) {
                                mCrosshairLocation.y--;
                                crosshairMoved = true;
                            }
                            break;
                        case SWT.ARROW_DOWN:
                            if (mCrosshairLocation.y != mHeight - 1) {
                                mCrosshairLocation.y++;
                                crosshairMoved = true;
                            }
                            break;
                        case SWT.ARROW_LEFT:
                            if (mCrosshairLocation.x != 0) {
                                mCrosshairLocation.x--;
                                crosshairMoved = true;
                            }
                            break;
                        case SWT.ARROW_RIGHT:
                            if (mCrosshairLocation.x != mWidth - 1) {
                                mCrosshairLocation.x++;
                                crosshairMoved = true;
                            }
                            break;
                    }
                }
            }
            if (crosshairMoved) {
                mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y);
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {
            // pass
        }

    };

    private PaintListener mPaintListener = new PaintListener() {
        @Override
        public void paintControl(PaintEvent e) {
            synchronized (PixelPerfectLoupe.this) {
                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
                e.gc.fillRectangle(0, 0, getSize().x, getSize().y);
                if (mImage != null && mCrosshairLocation != null) {
                    int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2;
                    int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2;
                    mTransform.translate(zoomedX, zoomedY);
                    mTransform.scale(mZoom, mZoom);
                    e.gc.setInterpolation(SWT.NONE);
                    e.gc.setTransform(mTransform);
                    e.gc.drawImage(mImage, 0, 0);
                    if (mShowOverlay && mOverlayImage != null) {
                        e.gc.setAlpha((int) (mOverlayTransparency * 255));
                        e.gc.drawImage(mOverlayImage, 0, mHeight - mOverlayImage.getBounds().height);
                        e.gc.setAlpha(255);
                    }

                    mTransform.identity();
                    e.gc.setTransform(mTransform);

                    // If the size of the canvas has changed, we need to make
                    // another grid.
                    if (mGrid != null
                            && (mCanvasWidth != getBounds().width || mCanvasHeight != getBounds().height)) {
                        mGrid.dispose();
                        mGrid = null;
                    }
                    mCanvasWidth = getBounds().width;
                    mCanvasHeight = getBounds().height;
                    if (mGrid == null) {
                        // Make a transparent image;
                        ImageData imageData =
                                new ImageData(mCanvasWidth + mZoom + 1, mCanvasHeight + mZoom + 1, 1,
                                        new PaletteData(new RGB[] {
                                            new RGB(0, 0, 0)
                                        }));
                        imageData.transparentPixel = 0;

                        // Draw the grid.
                        mGrid = new Image(Display.getDefault(), imageData);
                        GC gc = new GC(mGrid);
                        gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
                        for (int x = 0; x <= mCanvasWidth + mZoom; x += mZoom) {
                            gc.drawLine(x, 0, x, mCanvasHeight + mZoom);
                        }
                        for (int y = 0; y <= mCanvasHeight + mZoom; y += mZoom) {
                            gc.drawLine(0, y, mCanvasWidth + mZoom, y);
                        }
                        gc.dispose();
                    }

                    e.gc.setClipping(new Rectangle(zoomedX, zoomedY, mWidth * mZoom + 1, mHeight
                            * mZoom + 1));
                    e.gc.setAlpha(76);
                    e.gc.drawImage(mGrid, (mCanvasWidth / 2 - mZoom / 2) % mZoom - mZoom,
                            (mCanvasHeight / 2 - mZoom / 2) % mZoom - mZoom);
                    e.gc.setAlpha(255);

                    e.gc.setForeground(mCrosshairColor);
                    e.gc.drawLine(0, mCanvasHeight / 2, mCanvasWidth - 1, mCanvasHeight / 2);
                    e.gc.drawLine(mCanvasWidth / 2, 0, mCanvasWidth / 2, mCanvasHeight - 1);
                }
            }
        }
    };

    private void doRedraw() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                redraw();
            }
        });
    }

    private void loadImage() {
        mImage = mModel.getImage();
        if (mImage != null) {
            mWidth = mImage.getBounds().width;
            mHeight = mImage.getBounds().height;
        } else {
            mWidth = 0;
            mHeight = 0;
        }
    }

    // Note the syncExec and then synchronized... It avoids deadlock
    @Override
    public void imageLoaded() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    loadImage();
                    mCrosshairLocation = mModel.getCrosshairLocation();
                    mZoom = mModel.getZoom();
                    mOverlayImage = mModel.getOverlayImage();
                    mOverlayTransparency = mModel.getOverlayTransparency();
                }
            }
        });
        doRedraw();
    }

    @Override
    public void imageChanged() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    loadImage();
                }
            }
        });
        doRedraw();
    }

    @Override
    public void crosshairMoved() {
        synchronized (this) {
            mCrosshairLocation = mModel.getCrosshairLocation();
        }
        doRedraw();
    }

    @Override
    public void selectionChanged() {
        // pass
    }

    @Override
    public void treeChanged() {
        // pass
    }

    @Override
    public void zoomChanged() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    if (mGrid != null) {
                        // To notify that the zoom level has changed, we get rid
                        // of the
                        // grid.
                        mGrid.dispose();
                        mGrid = null;
                    }
                    mZoom = mModel.getZoom();
                }
            }
        });
        doRedraw();
    }

    @Override
    public void overlayChanged() {
        synchronized (this) {
            mOverlayImage = mModel.getOverlayImage();
            mOverlayTransparency = mModel.getOverlayTransparency();
        }
        doRedraw();
    }

    @Override
    public void overlayTransparencyChanged() {
        synchronized (this) {
            mOverlayTransparency = mModel.getOverlayTransparency();
        }
        doRedraw();
    }
}
