package org.sunflow.system;

import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;

import org.sunflow.core.Display;
import org.sunflow.image.Color;

@SuppressWarnings("serial")
public class ImagePanel extends JPanel implements Display {
    private static final int[] BORDERS = { Color.RED.toRGB(),
            Color.GREEN.toRGB(), Color.BLUE.toRGB(), Color.YELLOW.toRGB(),
            Color.CYAN.toRGB(), Color.MAGENTA.toRGB() };
    private BufferedImage image;
    private float xo, yo;
    private float w, h;
    private long repaintCounter;

    private class ScrollZoomListener extends MouseInputAdapter {
        int mx;
        int my;
        boolean dragging;
        boolean zooming;

        @Override
        public void mousePressed(MouseEvent e) {
            mx = e.getX();
            my = e.getY();
            switch (e.getButton()) {
                case MouseEvent.BUTTON1:
                    dragging = true;
                    zooming = false;
                    break;
                case MouseEvent.BUTTON2: {
                    dragging = zooming = false;
                    // if CTRL is pressed
                    if ((e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) == InputEvent.CTRL_DOWN_MASK)
                        fit();
                    else
                        reset();
                    break;
                }
                case MouseEvent.BUTTON3:
                    zooming = true;
                    dragging = false;
                    break;
                default:
                    return;
            }
            repaint();
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            int mx2 = e.getX();
            int my2 = e.getY();
            if (dragging)
                drag(mx2 - mx, my2 - my);
            if (zooming)
                zoom(mx2 - mx, my2 - my);
            mx = mx2;
            my = my2;
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            // same behaviour
            mouseDragged(e);
        }

        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
            zoom(-20 * e.getWheelRotation(), 0);
        }
    }

    public ImagePanel() {
        setPreferredSize(new Dimension(640, 480));
        image = null;
        xo = yo = 0;
        w = h = 0;
        ScrollZoomListener listener = new ScrollZoomListener();
        addMouseListener(listener);
        addMouseMotionListener(listener);
        addMouseWheelListener(listener);
    }

    public void save(String filename) {
        // Bitmap.save(image, filename);
        try {
            ImageIO.write(image, "png", new File(filename));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private synchronized void drag(int dx, int dy) {
        xo += dx;
        yo += dy;
        repaint();
    }

    private synchronized void zoom(int dx, int dy) {
        int a = Math.max(dx, dy);
        int b = Math.min(dx, dy);
        if (Math.abs(b) > Math.abs(a))
            a = b;
        if (a == 0)
            return;
        // window center
        float cx = getWidth() * 0.5f;
        float cy = getHeight() * 0.5f;

        // origin of the image in window space
        float x = xo + (getWidth() - w) * 0.5f;
        float y = yo + (getHeight() - h) * 0.5f;

        // coordinates of the pixel we are over
        float sx = cx - x;
        float sy = cy - y;

        // scale
        if (w + a > 100) {
            h = (w + a) * h / w;
            sx = (w + a) * sx / w;
            sy = (w + a) * sy / w;
            w = (w + a);
        }

        // restore center pixel

        float x2 = cx - sx;
        float y2 = cy - sy;

        xo = (x2 - (getWidth() - w) * 0.5f);
        yo = (y2 - (getHeight() - h) * 0.5f);

        repaint();
    }

    public synchronized void reset() {
        xo = yo = 0;
        if (image != null) {
            w = image.getWidth();
            h = image.getHeight();
        }
        repaint();
    }

    public synchronized void fit() {
        xo = yo = 0;
        if (image != null) {
            float wx = Math.max(getWidth() - 10, 100);
            float hx = wx * image.getHeight() / image.getWidth();
            float hy = Math.max(getHeight() - 10, 100);
            float wy = hy * image.getWidth() / image.getHeight();
            if (hx > hy) {
                w = wy;
                h = hy;
            } else {
                w = wx;
                h = hx;
            }
            repaint();
        }
    }

    public synchronized void imageBegin(int w, int h, int bucketSize) {
        if (image != null && w == image.getWidth() && h == image.getHeight()) {
            // dull image if it has same resolution (75%)
            for (int y = 0; y < h; y++) {
                for (int x = 0; x < w; x++) {
                    int rgba = image.getRGB(x, y);
                    image.setRGB(x, y, ((rgba & 0xFEFEFEFE) >>> 1) + ((rgba & 0xFCFCFCFC) >>> 2));
                }
            }
        } else {
            // allocate new framebuffer
            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
            // center
            this.w = w;
            this.h = h;
            xo = yo = 0;
        }
        repaintCounter = System.nanoTime();
        repaint();
    }

    public synchronized void imagePrepare(int x, int y, int w, int h, int id) {
        int border = BORDERS[id % BORDERS.length] | 0xFF000000;
        for (int by = 0; by < h; by++) {
            for (int bx = 0; bx < w; bx++) {
                if (bx == 0 || bx == w - 1) {
                    if (5 * by < h || 5 * (h - by - 1) < h)
                        image.setRGB(x + bx, y + by, border);
                } else if (by == 0 || by == h - 1) {
                    if (5 * bx < w || 5 * (w - bx - 1) < w)
                        image.setRGB(x + bx, y + by, border);
                }
            }
        }
        repaint();
    }

    public synchronized void imageUpdate(int x, int y, int w, int h, Color[] data, float[] alpha) {
        for (int j = 0, index = 0; j < h; j++)
            for (int i = 0; i < w; i++, index++)
                image.setRGB(x + i, y + j, data[index].copy().mul(1.0f / alpha[index]).toNonLinear().toRGBA(alpha[index]));
        repaint();
    }

    public synchronized void imageFill(int x, int y, int w, int h, Color c, float alpha) {
        int rgba = c.copy().mul(1.0f / alpha).toNonLinear().toRGBA(alpha);
        for (int j = 0, index = 0; j < h; j++)
            for (int i = 0; i < w; i++, index++)
                image.setRGB(x + i, y + j, rgba);
        fastRepaint();
    }

    public void imageEnd() {
        repaint();
    }

    private void fastRepaint() {
        long t = System.nanoTime();
        if (repaintCounter + 125000000 < t) {
            repaintCounter = t;
            repaint();
        }
    }

    @Override
    public synchronized void paintComponent(Graphics g) {
        super.paintComponent(g);
        if (image == null)
            return;
        int x = Math.round(xo + (getWidth() - w) * 0.5f);
        int y = Math.round(yo + (getHeight() - h) * 0.5f);
        int iw = Math.round(w);
        int ih = Math.round(h);
        int x0 = x - 1;
        int y0 = y - 1;
        int x1 = x + iw + 1;
        int y1 = y + ih + 1;
        g.setColor(java.awt.Color.WHITE);
        g.drawLine(x0, y0, x1, y0);
        g.drawLine(x1, y0, x1, y1);
        g.drawLine(x1, y1, x0, y1);
        g.drawLine(x0, y1, x0, y0);
        g.drawImage(image, x, y, iw, ih, java.awt.Color.BLACK, this);
    }
}