// {LICENSE}
/*
 * Copyright 2013-2015 HeroesGrave and other Spade developers.
 * 
 * This file is part of Spade
 * 
 * Spade is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

package heroesgrave.spade.gui;

import heroesgrave.spade.gui.menus.Menu;
import heroesgrave.spade.image.Document;
import heroesgrave.spade.image.Layer;
import heroesgrave.spade.image.RawImage;
import heroesgrave.spade.image.blend.BlendMode;
import heroesgrave.spade.image.change.IChange;
import heroesgrave.spade.image.change.IEditChange;
import heroesgrave.spade.image.change.IImageChange;
import heroesgrave.spade.image.change.IMaskChange;
import heroesgrave.spade.main.Spade;
import heroesgrave.utils.math.MathUtils;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.TexturePaint;
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.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;

import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JFrame;

/**
 * Handles:
 * - Handling of the Camera
 * - Drawing of any given Image.
 **/
@SuppressWarnings("serial")
public class PaintCanvas extends JComponent implements MouseListener, MouseMotionListener, MouseWheelListener
{
	public static final Color TRANSPARENT = new Color(255, 255, 255, 0);
	public static final int SELECTION_OVERLAY = 0x3f3f5f7f;
	public static BufferedImage backgroundLight, backgroundDark;
	
	// MISC
	final JFrame mainframe;
	BufferedImage image;
	
	// Camera
	float cam_zoom;
	float cam_positionX;
	float cam_positionY;
	
	// Controls
	int mouseX, mouseY, mouseLastDragPosX, mouseLastDragPosY;
	int lastButton;
	
	// Paint
	Rectangle2D backgroundRectangle;
	TexturePaint paintLight, paintDark;
	
	// Document rendering stuff 
	private BufferedImage frozen, unselected, preview, cachedTBG;
	private RawImage unselectedRaw, previewRaw;
	private boolean maskChanged;
	private int frozenTo;
	
	// Document
	Document document;
	
	static
	{
		try
		{
			backgroundLight = ImageIO.read(PaintCanvas.class.getResource("/res/tbg.png"));
			backgroundDark = ImageIO.read(PaintCanvas.class.getResource("/res/tbgd.png"));
		}
		catch(IOException e)
		{
			e.printStackTrace();
		}
	}
	
	public PaintCanvas(JFrame mainframe)
	{
		this.mainframe = mainframe;
		
		this.setMinimumSize(new Dimension(8, 8));
		this.setDoubleBuffered(true);
		this.setFocusable(true);
		
		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.addMouseWheelListener(this);
		
		this.cam_zoom = 1;
		this.cam_positionX = 0;
		this.cam_positionY = 0;
		
		this.updateBG();
		
		this.mouseLastDragPosX = 0;
		this.mouseLastDragPosY = 0;
	}
	
	public void cam_zoom_decrease()
	{
		float scale = this.cam_zoom;
		
		if(scale > 1 / 32f)
		{
			scale *= 0.95f;
		}
		
		// Keep the scale clean
		if(scale > 1f)
		{
			scale = MathUtils.floor(scale);
		}
		else if(scale < 1f)
		{
			scale = 1f / MathUtils.ceil(1f / scale);
		}
		
		this.setScale(scale);
	}
	
	public void cam_zoom_increase()
	{
		float scale = this.cam_zoom;
		
		if(scale < 64f)
		{
			scale /= 0.95f;
		}
		
		// Keep the scale clean
		if(scale > 1f)
		{
			scale = MathUtils.ceil(scale);
		}
		else if(scale < 1f)
		{
			scale = 1f / MathUtils.floor(1f / scale);
		}
		
		this.setScale(scale);
	}
	
	public float getScale()
	{
		return cam_zoom;
	}
	
	@Override
	public void mouseClicked(MouseEvent e)
	{
		if(document == null)
			return;
		if((e.getClickCount() == 2) && (e.getButton() == MouseEvent.BUTTON2))
		{
			this.cam_zoom = 1;
			this.cam_positionX = image.getWidth() / 2;
			this.cam_positionY = image.getHeight() / 2;
			this.repaint();
			this.updateBG();
		}
	}
	
	@Override
	public void mouseDragged(MouseEvent e)
	{
		if(document == null)
			return;
		// Don't use BUTTON2_MASK because it will trigger when ALT is pressed. Sometimes Java is stupid.
		int middleMouseMod = MouseEvent.BUTTON2_DOWN_MASK;
		int modifier = e.getModifiersEx();
		boolean middleMouseDown = (modifier & middleMouseMod) != 0;
		boolean returned = true;
		
		if(middleMouseDown && returned)
		{
			float dragX = e.getX() - this.mouseLastDragPosX;
			float dragY = e.getY() - this.mouseLastDragPosY;
			
			this.cam_positionX -= dragX / this.cam_zoom;
			this.cam_positionY -= dragY / this.cam_zoom;
			
			if(this.cam_positionX < 0)
			{
				this.cam_positionX = 0;
			}
			if(this.cam_positionY < 0)
			{
				this.cam_positionY = 0;
			}
			
			if(this.cam_positionX > this.image.getWidth())
			{
				this.cam_positionX = this.image.getWidth();
			}
			if(this.cam_positionY > this.image.getHeight())
			{
				this.cam_positionY = this.image.getHeight();
			}
			
			//this.updateBackground();
			this.repaint();
			returned = false;
		}
		
		if(returned && (lastButton == MouseEvent.BUTTON1 || lastButton == MouseEvent.BUTTON3))
		{
			Point2D p = this.transformCanvasPointToImagePoint(e.getPoint());
			short x = (short) p.getX();
			short y = (short) p.getY();
			mouseX = x;
			mouseY = y;
			Spade.main.gui.checkDynamicInfo();
			Spade.main.currentTool.whilePressed(document.getCurrent(), x, y, lastButton);
		}
		
		this.mouseLastDragPosX = e.getX();
		this.mouseLastDragPosY = e.getY();
	}
	
	@Override
	public void mouseEntered(MouseEvent e)
	{
		
	}
	
	@Override
	public void mouseExited(MouseEvent e)
	{
		
	}
	
	@Override
	public void mouseMoved(MouseEvent e)
	{
		if(document == null)
			return;
		Point _p = e.getPoint();
		Point2D p = this.transformCanvasPointToImagePoint(new Point2D.Float(_p.x, _p.y));
		short x = (short) p.getX();
		short y = (short) p.getY();
		mouseX = x;
		mouseY = y;
		Spade.main.gui.checkDynamicInfo();
		Spade.main.currentTool.whileReleased(document.getCurrent(), x, y, lastButton);
	}
	
	@Override
	public void mousePressed(MouseEvent e)
	{
		this.mouseLastDragPosX = e.getX();
		this.mouseLastDragPosY = e.getY();
		lastButton = e.getButton();
		
		if(document == null)
			return;
		if(e.getButton() == MouseEvent.BUTTON1 || e.getButton() == MouseEvent.BUTTON3)
		{
			Point _p = e.getPoint();
			Point2D p = this.transformCanvasPointToImagePoint(new Point2D.Float(_p.x, _p.y));
			short x = (short) p.getX();
			short y = (short) p.getY();
			mouseX = x;
			mouseY = y;
			Spade.main.gui.checkDynamicInfo();
			Spade.main.currentTool.onPressed(document.getCurrent(), x, y, e.getButton());
		}
	}
	
	@Override
	public void mouseReleased(MouseEvent e)
	{
		if(document == null)
			return;
		if(e.getButton() == MouseEvent.BUTTON1 || e.getButton() == MouseEvent.BUTTON3)
		{
			Point _p = e.getPoint();
			Point2D p = this.transformCanvasPointToImagePoint(new Point2D.Float(_p.x, _p.y));
			short x = (short) p.getX();
			short y = (short) p.getY();
			mouseX = x;
			mouseY = y;
			Spade.main.gui.checkDynamicInfo();
			Spade.main.currentTool.onReleased(document.getCurrent(), x, y, e.getButton());
		}
		lastButton = 0;
	}
	
	@Override
	public void mouseWheelMoved(MouseWheelEvent e)
	{
		int sign = e.getWheelRotation();
		
		if(sign < 0)
		{
			this.cam_zoom_increase();
			return;
		}
		
		if(sign > 0)
		{
			this.cam_zoom_decrease();
			return;
		}
	}
	
	@Override
	public void paint(Graphics _g)
	{
		Graphics2D g = (Graphics2D) _g;
		
		if(Menu.DARK_BACKGROUND)
		{
			g.setColor(new Color(0x1f1f1f));
			g.fillRect(0, 0, this.getWidth(), this.getHeight());
		}
		
		if(document == null)
			return;
		
		if(document.repaint)
			composeImage();
		
		renderImage(g);

		// Optionally draw the grid
		if(Menu.GRID_ENABLED && cam_zoom >= 8)
		{
			drawGrid(g);
		}
	}
	
	// Could probably be split up further, but I'll leave as-is for now.
	private void composeImage()
	{
		ArrayList<Layer> flatmap = document.getFlatMap();
		IChange previewChange = document.getPreview();
		
		// Create graphics and clear image.
		Graphics2D cg = image.createGraphics();
		cg.setBackground(PaintCanvas.TRANSPARENT);
		cg.clearRect(0, 0, image.getWidth(), image.getHeight());
		
		int index = flatmap.indexOf(document.getCurrent()) - 1;
		if(index >= 0)
		{
			if(Math.min(index, document.lowestChange - 1) < frozenTo)
			{
				Graphics2D fg = frozen.createGraphics();
				fg.setBackground(PaintCanvas.TRANSPARENT);
				fg.clearRect(0, 0, document.getWidth(), document.getHeight());
				for(int i = 0; i <= index; i++)
				{
					flatmap.get(i).render(fg);
				}
				document.lowestChange = frozenTo = index;
			}
			else if(index > frozenTo)
			{
				Graphics2D fg = frozen.createGraphics();
				for(int i = frozenTo + 1; i <= index; i++)
				{
					flatmap.get(i).render(fg);
				}
				document.lowestChange = frozenTo = index;
			}
			cg.drawImage(frozen, 0, 0, null);
		}
		else
		{
			frozenTo = -1;
			Graphics2D fg = frozen.createGraphics();
			fg.setBackground(PaintCanvas.TRANSPARENT);
			fg.clearRect(0, 0, document.getWidth(), document.getHeight());
		}
		
		boolean masked = true;
		
		if(previewChange != null)
		{
			previewRaw.copyFrom(document.getCurrent().getImage(), true);
			
			if(previewChange instanceof IEditChange)
			{
				((IEditChange) previewChange).apply(previewRaw);
				maskChanged = maskChanged || (previewChange instanceof IMaskChange);
			}
			else if(previewChange instanceof IImageChange)
			{
				previewRaw.copyFrom(((IImageChange) previewChange).apply(previewRaw), true);
			}
			
			if(previewRaw.isMaskEnabled())
			{
				if(maskChanged)
				{
					unselectedRaw.setMask(previewRaw.borrowMask());
					unselectedRaw.clear(SELECTION_OVERLAY);
					unselectedRaw.fill(0);
					maskChanged = false;
				}
			}
			else
			{
				masked = false;
			}
			
			// Draw Preview
			cg.setComposite(document.getCurrent().getBlendMode());
			cg.drawImage(preview, 0, 0, null);
		}
		else
		{
			// Render Current Layer
			flatmap.get(index + 1).render(cg);
			
			RawImage current = document.getCurrent().getImage();
			
			if(current.isMaskEnabled())
			{
				if(maskChanged)
				{
					unselectedRaw.setMask(current.borrowMask());
					unselectedRaw.clear(SELECTION_OVERLAY);
					unselectedRaw.fill(0);
					maskChanged = false;
				}
			}
			else
			{
				masked = false;
			}
		}
		
		if(masked) // Draw the selection overlay.
		{
			cg.setComposite(BlendMode.NORMAL);
			cg.drawImage(unselected, 0, 0, null);
		}
		
		for(int i = index + 2; i < flatmap.size(); i++)
		{
			flatmap.get(i).render(cg);
		}
		
		cg.dispose();
		document.repaint = false;
	}
	
	private void renderImage(Graphics2D g)
	{
		int translate_x = MathUtils.floor((-this.cam_positionX * this.cam_zoom) + this.getWidth()/2);
		int translate_y = MathUtils.floor((-this.cam_positionY * this.cam_zoom) + this.getHeight()/2);
		int width = MathUtils.floor(image.getWidth()*this.cam_zoom);
		int height = MathUtils.floor(image.getHeight()*this.cam_zoom);
		
		validateTBG();
		
		int left = translate_x;
		int top = translate_y;
		if(left < 0)
			left = ((left%32)-32)%32;
		if(top < 0)
			top = ((top%32)-32)%32;
		
		g.drawImage(cachedTBG, left, top, cachedTBG.getWidth(), cachedTBG.getHeight(), null);
		g.drawImage(image, translate_x, translate_y, width, height, null);
	}
	
	public void drawGrid(Graphics2D g)
	{
		int translate_x = MathUtils.floor((-this.cam_positionX * this.cam_zoom) + this.getWidth()/2);
		int translate_y = MathUtils.floor((-this.cam_positionY * this.cam_zoom) + this.getHeight()/2);
		
		int step = MathUtils.floor(this.cam_zoom);
		int tx = MathUtils.floor(this.cam_positionX * this.cam_zoom);
		int ty = MathUtils.floor(this.cam_positionY * this.cam_zoom);
		
		int top = Math.max(ty - this.getHeight() / 2, 0) / step * step;
		int bottom = Math.min(ty + this.getHeight() / 2, image.getHeight() * step);
		
		int left = Math.max(tx - this.getWidth() / 2, 0) / step * step;
		int right = Math.min(tx + this.getWidth() / 2, image.getWidth() * step);
		
		g.setColor(Color.gray);
		if(cam_zoom > 32)
			g.setStroke(new BasicStroke(1, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 1.0f, new float[]{cam_zoom * 0.25f, cam_zoom * 0.25f},
					cam_zoom * 0.125f));
		// Vertical
		for(int i = left; i < right; i += step)
		{
			g.drawLine(translate_x+i, translate_y+top, translate_x+i, translate_y+bottom);
		}
		// Horizontal
		for(int i = top; i < bottom; i += step)
		{
			g.drawLine(translate_x+left, translate_y+i, translate_x+right, translate_y+i);
		}
	}
	
	public void maskChanged()
	{
		this.maskChanged = true;
	}
	
	public void validateTBG()
	{
		int targetWidth = Math.min(MathUtils.floor(image.getWidth()*cam_zoom), this.getWidth()+32);
		int targetHeight = Math.min(MathUtils.floor(image.getHeight()*cam_zoom), this.getHeight()+32);
		
		if(cachedTBG != null && cachedTBG.getWidth() == targetWidth && cachedTBG.getHeight() == targetHeight)
			return;
		
		this.cachedTBG = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_ARGB);
		
		Graphics2D g = cachedTBG.createGraphics();
		g.scale(cam_zoom, cam_zoom);
		if(Menu.DARK_BACKGROUND)
		{
			g.setPaint(paintDark);
		}
		else
		{
			g.setPaint(paintLight);
		}
		g.fillRect(0, 0, cachedTBG.getWidth(), cachedTBG.getHeight());
	}
	
	public void resized(int width, int height)
	{
		this.image = document.getRenderedImage();
		this.frozen = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
		this.unselected = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
		this.unselectedRaw = RawImage.unwrapBufferedImage(unselected);
		this.preview = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
		this.previewRaw = RawImage.unwrapBufferedImage(preview);
		this.cam_zoom = 1;
		this.cam_positionX = image.getWidth() / 2;
		this.cam_positionY = image.getHeight() / 2;
		this.repaint();
		Spade.main.gui.info.setSize(width, height);
	}
	
	public void setDocument(Document document)
	{
		this.document = document;
		if(document == null)
		{
			unselectedRaw = previewRaw = null;
			unselected = preview = frozen = image = null;
			this.cam_positionX = 0;
			this.cam_positionY = 0;
		}
		else
		{
			this.image = document.getRenderedImage();
			this.frozen = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
			this.unselected = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
			this.unselectedRaw = RawImage.unwrapBufferedImage(unselected);
			this.preview = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
			this.previewRaw = RawImage.unwrapBufferedImage(preview);
			this.cam_positionX = image.getWidth() / 2;
			this.cam_positionY = image.getHeight() / 2;
			Spade.main.gui.checkDynamicInfo();
		}
		this.repaint();
	}
	
	public void setScale(float scale)
	{
		this.cam_zoom = scale;
		this.updateBG();
		this.repaint();
		Spade.main.gui.checkDynamicInfo();
	}
	
	public final Point2D.Float transformCanvasPointToImagePoint(Point2D in)
	{
		float x = (float) in.getX();
		float y = (float) in.getY();
		x -= this.getWidth()/2;
		y -= this.getHeight()/2;
		x /= this.cam_zoom;
		y /= this.cam_zoom;
		x += this.cam_positionX;
		y += this.cam_positionY;
		
		return new Point2D.Float(x, y);
	}

	public final Point2D.Float transformImagePointToCanvasPoint(Point2D in)
	{
		float x = (float) in.getX();
		float y = (float) in.getY();
		x -= this.cam_positionX;
		y -= this.cam_positionY;
		x *= this.cam_zoom;
		y *= this.cam_zoom;
		x += this.getWidth()/2;
		y += this.getHeight()/2;
		
		return new Point2D.Float(x, y);
	}
	
	private void updateBG()
	{
		Rectangle2D.Float rect = new Rectangle2D.Float(0, 0, 16f / cam_zoom, 16f / cam_zoom);
		paintLight = new TexturePaint(backgroundLight, rect);
		paintDark = new TexturePaint(backgroundDark, rect);
	}
}