// Copyright (C) 2010, 2011, 2012 GlavSoft LLC.
// All rights reserved.
//
//-------------------------------------------------------------------------
// This file is part of the TightVNC software.  Please visit our Web site:
//
//                       http://www.tightvnc.com/
//
// This program 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 2 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, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//-------------------------------------------------------------------------
//

package com.glavsoft.viewer.swing;

import com.glavsoft.core.SettingsChangedEvent;
import com.glavsoft.drawing.Renderer;
import com.glavsoft.rfb.IChangeSettingsListener;
import com.glavsoft.rfb.IRepaintController;
import com.glavsoft.rfb.encoding.PixelFormat;
import com.glavsoft.rfb.encoding.decoder.FramebufferUpdateRectangle;
import com.glavsoft.rfb.protocol.ProtocolContext;
import com.glavsoft.rfb.protocol.ProtocolSettings;
import com.glavsoft.transport.Reader;
import com.glavsoft.viewer.Viewer;

import javax.swing.*;
import java.awt.*;

// BEGIN added
import net.jmge.gif.*;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.awt.image.DataBuffer;
import javax.xml.bind.DatatypeConverter;
// END added

@SuppressWarnings("serial")
public class Surface extends JPanel implements IRepaintController, IChangeSettingsListener {

	private int width;
	private int height;
	private SoftCursorImpl cursor;
	private RendererImpl renderer;
	private MouseEventListener mouseEventListener;
	private KeyEventListener keyEventListener;
	private boolean showCursor;
	private ModifierButtonEventListener modifierButtonListener;
	private boolean isUserInputEnabled = false;
	private final ProtocolContext context;
	private double scaleFactor;
	private final Viewer viewer;
	public Dimension oldSize;

	@Override
	public boolean isDoubleBuffered() {
		// TODO returning false in some reason may speed ups drawing, but may
		// not. Needed in challenging.
		return false;
	}

	public Surface(ProtocolContext context, Viewer viewer, double scaleFactor) {
		this.context = context;
		this.viewer = viewer;
		this.scaleFactor = scaleFactor;
		init(context.getFbWidth(), context.getFbHeight());
		oldSize = getPreferredSize();

		if ( ! context.getSettings().isViewOnly()) {
			setUserInputEnabled(true, context.getSettings().isConvertToAscii());
		}
		showCursor = context.getSettings().isShowRemoteCursor();
	}

	private void setUserInputEnabled(boolean enable, boolean convertToAscii) {
		if (enable == isUserInputEnabled) return;
		isUserInputEnabled = enable;
		if (enable) {
			if (null == mouseEventListener) {
				mouseEventListener = new MouseEventListener(this, context, scaleFactor);
			}
			addMouseListener(mouseEventListener);
			addMouseMotionListener(mouseEventListener);
			addMouseWheelListener(mouseEventListener);

			setFocusTraversalKeysEnabled(false);
			if (null == keyEventListener) {
				keyEventListener = new KeyEventListener(context);
				if (modifierButtonListener != null) {
					keyEventListener.addModifierListener(modifierButtonListener);
				}
			}
			keyEventListener.setConvertToAscii(convertToAscii);
			addKeyListener(keyEventListener);
			enableInputMethods(false);
		} else {
			removeMouseListener(mouseEventListener);
			removeMouseMotionListener(mouseEventListener);
			removeMouseWheelListener(mouseEventListener);
			removeKeyListener(keyEventListener);
		}
	}

	@Override
	public Renderer createRenderer(Reader reader, int width, int height, PixelFormat pixelFormat) {
		renderer = new RendererImpl(reader, width, height, pixelFormat);
		synchronized (renderer) {
			cursor = renderer.getCursor();
		}
		init(renderer.getWidth(), renderer.getHeight());
		updateFrameSize();
		return renderer;
	}

	private void init(int width, int height) {
		this.width = width;
		this.height = height;
		setSize(getPreferredSize());
	}

	private void updateFrameSize() {
		setSize(getPreferredSize());
		viewer.packContainer();
		requestFocus();
	}
	// BEGIN added
	private static final int[] colourMap
		= {
			0x00000000, 
			0x11111111,
			0x22222222, 
			0x33333333, 
			0x44444444, 
			0x55555555, 
			0x66666666, 
			0x77777777, 
			0x88888888, 
			0x99999999, 
			0xAAAAAAAA, 
			0xBBBBBBBB, 
			0xCCCCCCCC, 
			0xDDDDDDDD, 
			0xEEEEEEEE, 
			0xFFFFFFFF, 
		};
	private static final IndexColorModel colorModel = new IndexColorModel(4, 16, colourMap, 0, false, 0, DataBuffer.TYPE_BYTE);
	public String getOffscreenImageAsBase64 () {
		String retval = "";
		try {
			BufferedImage image = new BufferedImage(width, height,  
					BufferedImage.TYPE_BYTE_BINARY, colorModel);  
			Graphics g = image.getGraphics();  
			synchronized (renderer) {
				g.drawImage(renderer.getOffscreenImage(), 0, 0, null);  
			}
			synchronized (cursor) {
				Image cursorImage = cursor.getImage();
				g.drawImage(cursorImage, cursor.rX, cursor.rY, null);
			}
			g.dispose(); 

			ByteArrayOutputStream bout = new ByteArrayOutputStream();
			Gif89Encoder gifenc = new Gif89Encoder(image);
			gifenc.setTransparentIndex(-1);
			gifenc.getFrameAt(0).setInterlaced(false);
			gifenc.encode(bout);

			retval = DatatypeConverter.printBase64Binary(bout.toByteArray());
		} catch (IOException e) {
			e.printStackTrace();
		}
		return retval;
	}
	// END added

	@Override
	public void paintComponent(Graphics g) {
		((Graphics2D)g).scale(scaleFactor, scaleFactor);
		((Graphics2D) g).setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
		synchronized (renderer) {
			Image offscreenImage = renderer.getOffscreenImage();
			if (offscreenImage != null) {
				g.drawImage(offscreenImage, 0, 0, null);
			}
		}
		synchronized (cursor) {
			Image cursorImage = cursor.getImage();
			if (showCursor && cursorImage != null &&
					(scaleFactor != 1 ||
							g.getClipBounds().intersects(cursor.rX, cursor.rY, cursor.width, cursor.height))) {
				g.drawImage(cursorImage, cursor.rX, cursor.rY, null);
			}
		}
	}

	@Override
	public Dimension getPreferredSize() {
		return new Dimension((int)(this.width * scaleFactor), (int)(this.height * scaleFactor));
	}

	@Override
	public Dimension getMinimumSize() {
		return getPreferredSize();
	}

	@Override
	public Dimension getMaximumSize() {
		return getPreferredSize();
	}

	/**
	 * Saves context and simply invokes native JPanel repaint method which
	 * asyncroniously register repaint request using invokeLater to repaint be
	 * runned in Swing event dispatcher thread. So may be called from other
	 * threads.
	 */
	@Override
	public void repaintBitmap(FramebufferUpdateRectangle rect) {
		repaintBitmap(rect.x, rect.y, rect.width, rect.height);
	}

	@Override
	public void repaintBitmap(int x, int y, int width, int height) {
		repaint((int)(x * scaleFactor), (int)(y * scaleFactor),
                (int)Math.ceil(width * scaleFactor), (int)Math.ceil(height * scaleFactor));
	}

	@Override
	public void repaintCursor() {
		synchronized (cursor) {
			repaint((int)(cursor.oldRX * scaleFactor), (int)(cursor.oldRY * scaleFactor),
					(int)Math.ceil(cursor.oldWidth * scaleFactor) + 1, (int)Math.ceil(cursor.oldHeight * scaleFactor) + 1);
			repaint((int)(cursor.rX * scaleFactor), (int)(cursor.rY * scaleFactor),
					(int)Math.ceil(cursor.width * scaleFactor) + 1, (int)Math.ceil(cursor.height * scaleFactor) + 1);
		}
	}

	@Override
	public void updateCursorPosition(short x, short y) {
		synchronized (cursor) {
			cursor.updatePosition(x, y);
			repaintCursor();
		}
	}

	private void showCursor(boolean show) {
		synchronized (cursor) {
			showCursor = show;
		}
	}

	public void addModifierListener(ModifierButtonEventListener modifierButtonListener) {
		this.modifierButtonListener = modifierButtonListener;
		if (keyEventListener != null) {
			keyEventListener.addModifierListener(modifierButtonListener);
		}
	}

	@Override
	public void settingsChanged(SettingsChangedEvent e) {
		if (ProtocolSettings.isRfbSettingsChangedFired(e)) {
			ProtocolSettings settings = (ProtocolSettings) e.getSource();
			setUserInputEnabled( ! settings.isViewOnly(), settings.isConvertToAscii());
			showCursor(settings.isShowRemoteCursor());
		} else if (UiSettings.isUiSettingsChangedFired(e)) {
			UiSettings settings = (UiSettings) e.getSource();
			oldSize = getPreferredSize();
			scaleFactor = settings.getScaleFactor();
		}
		mouseEventListener.setScaleFactor(scaleFactor);
		updateFrameSize();
	}

	@Override
	public void setPixelFormat(PixelFormat pixelFormat) {
		if (renderer != null) {
			renderer.initPixelFormat(pixelFormat);
		}
	}

}
