/*
 * Copyright (c) 1997 - 2016
 * Actelion Pharmaceuticals Ltd.
 * Gewerbestrasse 16
 * CH-4123 Allschwil, Switzerland
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the the copyright holder nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author Thomas Sander
 */

package com.actelion.research.gui;

import com.actelion.research.chem.*;
import com.actelion.research.chem.io.CompoundFileHelper;
import com.actelion.research.chem.name.StructureNameResolver;
import com.actelion.research.gui.clipboard.IClipboardHandler;
import com.actelion.research.gui.dnd.MoleculeDragAdapter;
import com.actelion.research.gui.dnd.MoleculeDropAdapter;
import com.actelion.research.gui.dnd.MoleculeTransferable;
import com.actelion.research.gui.generic.GenericDepictor;
import com.actelion.research.gui.generic.GenericPolygon;
import com.actelion.research.gui.generic.GenericRectangle;
import com.actelion.research.gui.generic.GenericShape;
import com.actelion.research.gui.hidpi.HiDPIHelper;
import com.actelion.research.gui.swing.SwingCursorHelper;
import com.actelion.research.gui.swing.SwingDrawContext;
import com.actelion.research.util.ColorHelper;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.*;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.io.*;
import java.util.ArrayList;

public class JStructureView extends JComponent implements ActionListener,MouseListener,MouseMotionListener,StructureListener {
    private static final long serialVersionUID = 0x20061113;
	private static final Color DEFAULT_SELECTION_BACKGROUND = new Color(128,164,192);

    private static final String ITEM_COPY = "Copy Structure";
	private static final String ITEM_COPY_SMILES = "Copy Structure As SMILES-String";
	private static final String ITEM_PASTE= "Paste Structure";
	private static final String ITEM_PASTE_WITH_NAME = ITEM_PASTE+" or Name";
	private static final String ITEM_CLEAR = "Clear Structure";
	private static final String ITEM_OPEN_MOLFILE = "Open Molfile...";
	private static final String ITEM_SAVE_MOLFILE_V2 = "Save As Molfile V2...";
	private static final String ITEM_SAVE_MOLFILE_V3 = "Save As Molfile V3...";

	private static final long WARNING_MILLIS = 1200;

	private static final int DRAG_MARGIN = 12;

	private ArrayList<StructureListener> mListener;
	private String mIDCode;
	private StereoMolecule mMol,mDisplayMol;
    private GenericDepictor mDepictor;
	private boolean mShowBorder,mAllowFragmentStatusChangeOnPasteOrDrop,mIsDraggingThis,mIsEditable,mDisableBorder,
			mIsSelectable,mIsSelecting,mIsLassoSelect;
	private int mChiralTextPosition,mDisplayMode;
	private String[] mAtomText;
	private IClipboardHandler mClipboardHandler;
	protected MoleculeDropAdapter mDropAdapter = null;
	protected int mAllowedDragAction;
	protected int mAllowedDropAction;
	private String mWarningMessage;
	private int[] mAtomHiliteColor;
	private float[] mAtomHiliteRadius;
	private double mTextSizeFactor,mX1,mY1,mX2,mY2;
	private GenericPolygon mLassoRegion;


	public JStructureView() {
        this(null);
		}

	/**
	 * This creates a standard structure view where the displayed molecule is
	 * used for D&D and clipboard transfer after removing atom colors and bond highlights.
	 * The default will support copy/paste and drag&drop from this view only,
	 * but dropping anything onto this view doesn't have an effect.
	 * Call setEditable(true) to allow changes through drag&drop and pasting.
	 * @param mol used for display, clipboard copy and d&d
	 */
	public JStructureView(StereoMolecule mol) {
        this(mol, DnDConstants.ACTION_COPY_OR_MOVE, DnDConstants.ACTION_COPY_OR_MOVE);
	    }

	/**
	 * This creates a structure view that distinguishes between displayed molecule
	 * and the one being used for D&D and clipboard transfer. Use this if the displayed
	 * molecule is structurally different, e.g. uses custom atom labels or additional
	 * illustrative atoms or bonds, which shall not be copied.
	 * Custom atom colors or highlighted bonds don't require a displayMol.
	 * The default will support copy/paste and drag&drop FROM this view only,
	 * but dropping anything onto this view doesn't have an effect.
	 * Call setEditable(true) to allow changes through drag&drop and pasting.
	 * @param mol used for clipboard copy and d&d; used for display if displayMol is null
	 * @param displayMol null if mol shall be displayed
	 */
	public JStructureView(StereoMolecule mol, StereoMolecule displayMol) {
        this(mol, displayMol, DnDConstants.ACTION_COPY_OR_MOVE, DnDConstants.ACTION_COPY_OR_MOVE);
	    }

	public JStructureView(int dragAction, int dropAction) {
        this(null, dragAction, dropAction);
	    }

	/**
	 * This creates a standard structure view where the displayed molecule is
	 * used for D&D and clipboard transfer after removing atom colors and bond highlights.
	 * The default will support copy/paste and drag&drop from this view only,
	 * but dropping anything onto this view doesn't have an effect.
	 * Call setEditable(true) to allow changes through drag&drop and pasting.
	 * @param mol used for display, clipboard copy and d&d
	 * @param dragAction
	 * @param dropAction
	 */
	public JStructureView(StereoMolecule mol, int dragAction, int dropAction) {
        this(mol, null, dragAction, dropAction);
		}

	/**
	 * This creates a structure view that distinguishes between displayed molecule
	 * and the one being used for D&D and clipboard transfer. Use this if the displayed
	 * molecule is structurally different, e.g. uses custom atom labels or additional
	 * illustrative atoms or bonds, which shall not be copied.
	 * Custom atom colors or highlighted bonds don't require a displayMol.
	 * The default will support copy/paste and drag&drop from this view only,
	 * but dropping anything onto this view doesn't have an effect.
	 * Call setEditable(true) to allow changes through drag&drop and pasting.
	 * @param mol used for clipboard copy and d&d; used for display if displayMol is null
	 * @param displayMol null if mol shall be displayed
	 * @param dragAction
	 * @param dropAction
	 */
	public JStructureView(StereoMolecule mol, StereoMolecule displayMol, int dragAction, int dropAction) {
		mMol = (mol == null) ? new StereoMolecule() : new StereoMolecule(mol);
		mDisplayMol = (displayMol == null) ? mMol : displayMol;
		mDisplayMode = AbstractDepictor.cDModeHiliteAllQueryFeatures;
		mTextSizeFactor = 1.0;
		mIsEditable = false;
		updateBackground();
		addMouseListener(this);
		addMouseMotionListener(this);
		initializeDragAndDrop(dragAction, dropAction);
	    }

	public void setSelectable(boolean s) {
		mIsSelectable = s;
		}

	@Override
	public void updateUI() {
		super.updateUI();
		updateBackground();
		}

    /**
     * Call this in order to get clipboard support:
     * setClipboardHandler(new ClipboardHandler());
     */
	public void setClipboardHandler(IClipboardHandler h) {
		mClipboardHandler = h;
	    }

	public IClipboardHandler getClipboardHandler() {
		return mClipboardHandler;
	    }

	public int getDisplayMode() {
		return mDisplayMode;
		}

	/**
	 * Sets the display mode for the Depictor. The default is
	 * AbstractDepictor.cDModeHiliteAllQueryFeatures.
	 * @param mode
	 */
	public void setDisplayMode(int mode) {
		if (mDisplayMode != mode) {
		    mDisplayMode = mode;
		    repaint();
			}
	    }

	/**
	 * Sets a multiplication factor to the text size of all labels. The default is 1.0.
	 * @param factor text size factor
	 */
	public void setFactorTextSize(double factor) {
		mTextSizeFactor = factor;
		}

	public void setDisableBorder(boolean b) {
		mDisableBorder = b;
		}

	/**
	 * Defines additional atom text to be displayed in top right
	 * position of some/all atom labels. If the atom is charged, then
	 * the atom text is drawn right of the atom charge.
	 * If using atom text make sure to update it accordingly, if atom
	 * indexes change due to molecule changes.
	 * Atom text is not supported for MODE_REACTION, MODE_MULTIPLE_FRAGMENTS or MODE_MARKUSH_STRUCTURE.
	 * @param atomText null or String array matching atom indexes (may contain null entries)
	 */
	public void setAtomText(String[] atomText) {
		mAtomText = atomText;
		}

	public void setEnabled(boolean enable) {
		if (enable != isEnabled()) {
			updateBackground();
			repaint();
			if (mDropAdapter != null)
				mDropAdapter.setActive(enable);
			}
		super.setEnabled(enable);
		}


	public boolean isEditable() {
		return mIsEditable;
	}

	public void setEditable(boolean b) {
		if (mIsEditable != b)
			mIsEditable = b;
		}

	/**
	 * When fragment status change on drop is allowed then dropping a fragment (molecule)
	 * on a molecule (fragment) inverts the status of the view's chemical object.
	 * As default status changes are prohibited.
	 * @param allow
	 */
	public void setAllowFragmentStatusChangeOnPasteOrDrop(boolean allow) {
		mAllowFragmentStatusChangeOnPasteOrDrop = allow;
		}

	/**
	 * If you want this structure view to also draw an atom background with specific colors for every atom,
	 * then you need to call this method before or just after one of the structureChanged() calls.
	 * @param argb if alpha < 1 then the background is mixed in accordingly
	 * @param radius <= 1.0; if null, then a default of 0.5 of the average bond length is used
	 */
	public void setAtomHighlightColors(int[] argb, float[] radius) {
		mAtomHiliteColor = argb;
		mAtomHiliteRadius = radius;
		repaint();
		}

	public boolean canDrop() {
		return mIsEditable && isEnabled() && !mIsDraggingThis;
	    }

	@Override
	public synchronized void paintComponent(Graphics g) {
        super.paintComponent(g);

        Dimension theSize = getSize();
		Insets insets = getInsets();
		theSize.width -= insets.left + insets.right;
		theSize.height -= insets.top + insets.bottom;

        if (theSize.width <= 0 || theSize.height <= 0)
            return;

        Graphics2D g2 = (Graphics2D)g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
        g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);

		Color fg = g2.getColor();
		Color bg = getBackground();
		g2.setColor(bg);
		g2.fill(new Rectangle(insets.left, insets.top, theSize.width, theSize.height));

		if (mShowBorder && !mDisableBorder) {
			GenericRectangle rect = mDepictor.getBoundingRect();
			if (rect != null) {
				g.setColor(ColorHelper.perceivedBrightness(bg) < 0.5f ? ColorHelper.brighter(bg, 0.85f) : ColorHelper.darker(bg, 0.85f));
				int arc = (int)Math.min(rect.height/4, Math.min(rect.width/4, HiDPIHelper.scale(10)));
				g.fillRoundRect((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height, arc, arc);
				}
			}

		g2.setColor(fg);

		if (mDisplayMol != null && mDisplayMol.getAllAtoms() != 0) {
			mDepictor = new GenericDepictor(mDisplayMol);
            mDepictor.setDisplayMode(mDisplayMode);
            mDepictor.setFactorTextSize(mTextSizeFactor);
            mDepictor.setAtomText(mAtomText);
            mDepictor.setAtomHighlightColors(mAtomHiliteColor, mAtomHiliteRadius);

            int bgRGB = bg.getRGB();
			if (!isEnabled())
                mDepictor.setOverruleColor(ColorHelper.getContrastColor(0x808080, bgRGB), bgRGB);
			else
				mDepictor.setForegroundColor(getForeground().getRGB(), bgRGB);

			int avbl = HiDPIHelper.scale(AbstractDepictor.cOptAvBondLen);
			SwingDrawContext context = new SwingDrawContext((Graphics2D)g);
			mDepictor.validateView(context, new GenericRectangle(insets.left, insets.top, theSize.width,theSize.height),
								   AbstractDepictor.cModeInflateToMaxAVBL | mChiralTextPosition | avbl);
            mDepictor.paint(context);
			}

		if (mWarningMessage != null) {
			int fontSize = HiDPIHelper.scale(12);
			g.setFont(getFont().deriveFont(Font.BOLD, (float)fontSize));
			Color original = g.getColor();
			g.setColor(Color.RED);
			FontMetrics metrics = g.getFontMetrics();
			Rectangle2D bounds = metrics.getStringBounds(mWarningMessage, g);
			g.drawString(mWarningMessage, insets.left+(int)(theSize.width-bounds.getWidth())/2,
					insets.top+metrics.getHeight());
			g.setColor(original);
			}

		if (mIsSelecting) {
			if (mIsLassoSelect) {
				g.setColor(lassoColor());
				java.awt.Polygon p = new java.awt.Polygon();
				for (int i = 0; i<mLassoRegion.getSize(); i++)
					p.addPoint(Math.round((float)mLassoRegion.getX(i)), Math.round((float)mLassoRegion.getY(i)));
				g.drawPolygon(p);
				g.setColor(getForeground());
			}
			else {
				int x = (mX1 < mX2) ? (int) mX1 : (int) mX2;
				int y = (mY1 < mY2) ? (int) mY1 : (int) mY2;
				int w = (int) Math.abs(mX2 - mX1);
				int h = (int) Math.abs(mY2 - mY1);
				g.setColor(lassoColor());
				g.drawRect(x, y, w, h);
				g.setColor(getForeground());
				}
			}
		}

	private Color lassoColor() {
		Color selectionColor = selectionColor();
		return ColorHelper.createColor(selectionColor, LookAndFeelHelper.isDarkLookAndFeel() ? 0.65f : 0.35f);
	}

	private Color selectionColor() {
		Color selectionColor = UIManager.getColor("TextArea.selectionBackground");
		return (selectionColor != null) ? selectionColor : DEFAULT_SELECTION_BACKGROUND;
	}

	public void setIDCode(String idcode) {
		int index = (idcode == null) ? -1 : idcode.indexOf(' ');
		if (index == -1)
			setIDCode(idcode, null);
		else
			setIDCode(idcode.substring(0, index), idcode.substring(index+1));
	    }

	public synchronized void setIDCode(String idcode, String coordinates) {
		if (idcode != null && idcode.isEmpty())
			idcode = null;

		if (mIDCode == null && idcode == null)
			return;

		if (mIDCode != null && mIDCode.equals(idcode))
			return;

		new IDCodeParser(true).parse(mMol, idcode, coordinates);
		mDisplayMol = mMol;

        mIDCode = idcode;
        repaint();
        informListeners();
		}

	/**
	 * Updates the molecule used for display, drag & drop and clipboard transfer.
	 * Also triggers a repaint().
	 * @param mol new molecule used for display, clipboard copy and d&d; may be null
	 */
	public synchronized void structureChanged(StereoMolecule mol) {
		if (mol == null) {
			mMol.clear();
			}
		else {
			mol.copyMolecule(mMol);
			}

		mDisplayMol = mMol;
        structureChanged();
		}

	/**
	 * Updates both molecules used for display and for drag & drop/clipboard transfer.
	 * Also triggers a repaint().
	 * @param mol new molecule used for display; may be null
	 * @param displayMol new molecule used for clipboard copy and d&d, may be null
	 */
	public synchronized void structureChanged(StereoMolecule mol, StereoMolecule displayMol) {
		if (mol == null) {
			mMol.clear();
			}
		else {
			mol.copyMolecule(mMol);
			}

		mDisplayMol = displayMol;
        structureChanged();
		}

	/**
	 * Should only be called if JStructureView's internal Molecule is changed
	 * from outside as: theStructureView.getMolecule().setFragment(false);
	 * The caller is responsible to update displayMol also, if it is different from
	 * the molecule.
	 */
	public synchronized void structureChanged() {
		mIDCode = null;
		repaint();
		informListeners();
		}

	public StereoMolecule getMolecule() {
		return mMol;
		}

	public StereoMolecule getDisplayMolecule() {
		return mDisplayMol;
		}

    public AbstractDepictor getDepictor() {
        return mDepictor;
        }

    public void addStructureListener(StructureListener l) {
		if(mListener == null)
			mListener = new ArrayList<>();

		mListener.add(l);
		}

    public void removeStructureListener(StructureListener l) {
        if(mListener != null)
            mListener.remove(l);
        }

	public void setChiralDrawPosition(int p) {
		mChiralTextPosition = p;
		}

	@Override public void mouseClicked(MouseEvent e) {}
	@Override public void mouseEntered(MouseEvent e) {}
	@Override public void mouseExited(MouseEvent e) {}

	@Override
	public void mousePressed(MouseEvent e) {
		if (handlePopupTrigger(e))
			return;

		if (mIsSelectable
		 && (e.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
			mX1 = e.getX();
			mY1 = e.getY();
			mIsSelecting = true;
			mIsLassoSelect = !e.isAltDown();
			if (mIsLassoSelect) {
				mLassoRegion = new GenericPolygon();
				mLassoRegion.addPoint(mX1, mY1);
				mLassoRegion.addPoint(mX1, mY1);
				}
			}
		}

	@Override
	public void mouseReleased(MouseEvent e) {
		if (mIsSelecting) {
			mIsSelecting = false;
			repaint();
			}

		handlePopupTrigger(e);
		}

	@Override
	public void mouseMoved(MouseEvent e) {
		int x = e.getX();
		int y = e.getY();
		boolean isInRect = mDepictor != null
				&& (mAllowedDragAction & DnDConstants.ACTION_COPY) != 0
				&& shrink(mDepictor.getBoundingRect()).contains(x, y);

		updateBorder(isInRect);
		setCursor(SwingCursorHelper.getCursor(isInRect ? SwingCursorHelper.cHandCursor : SwingCursorHelper.cPointerCursor));
		}

	private GenericRectangle shrink(GenericRectangle rect) {
		int margin = HiDPIHelper.scale(DRAG_MARGIN);
		int marginX = Math.min(margin, (int)rect.width / 6);
		int marginY = Math.min(margin, (int)rect.height / 6);
		return new GenericRectangle((int)rect.x+marginX, (int)rect.y+marginY, (int)rect.width-2*marginX, (int)rect.height-2*marginY);
	}

	@Override public void mouseDragged(MouseEvent e) {
		if (mIsSelecting) {
			mX2 = e.getX();
			mY2 = e.getY();

			GenericShape selectedShape = null;

			if (mIsLassoSelect) {
				if ((Math.abs(mX2 - mLassoRegion.getX(mLassoRegion.getSize() - 1)) >= 3)
				 || (Math.abs(mY2 - mLassoRegion.getY(mLassoRegion.getSize() - 1)) >= 3)) {
					mLassoRegion.removeLastPoint();
					mLassoRegion.addPoint(mX2, mY2);
					mLassoRegion.addPoint(mX1, mY1);

					selectedShape = mLassoRegion;
					}
				}
			else {
				selectedShape = new GenericRectangle(Math.min(mX1, mX2), Math.min(mY1, mY2), Math.abs(mX2 - mX1), Math.abs(mY2 - mY1));
				}

			if (selectedShape != null && mDepictor != null) {
				for (int atom=0; atom<mDisplayMol.getAllAtoms(); atom++) {
					boolean isSelected = selectedShape.contains((int)mDepictor.getAtomX(atom), (int)mDepictor.getAtomY(atom));
					if (isSelected != mDisplayMol.isSelectedAtom(atom)) {
						mDisplayMol.setAtomSelection(atom, isSelected);
						}
					}
				repaint();
				}
			}
		}

	public void actionPerformed(ActionEvent e) {
		if (e.getActionCommand().equals(ITEM_COPY)) {
			mClipboardHandler.copyMolecule(mMol);
			}
		if (e.getActionCommand().equals(ITEM_COPY_SMILES)) {
			final String smiles = new IsomericSmilesCreator(mMol).getSmiles();
			final StringSelection data = new StringSelection(smiles);
			Toolkit.getDefaultToolkit().getSystemClipboard().setContents(data, data);
			}
		if (e.getActionCommand().startsWith(ITEM_PASTE) && mIsEditable) {
			int smartsMode = mAllowFragmentStatusChangeOnPasteOrDrop ? SmilesParser.SMARTS_MODE_GUESS
						   : mMol.isFragment() ? SmilesParser.SMARTS_MODE_IS_SMARTS : SmilesParser.SMARTS_MODE_IS_SMILES;
			StereoMolecule mol = mClipboardHandler.pasteMolecule(true, smartsMode);
			if (mol != null) {
				if (!mAllowFragmentStatusChangeOnPasteOrDrop)
					mol.setFragment(mMol.isFragment());
				mMol = mol;
				mDisplayMol = mol;
				structureChanged();
				}
			else {
				showWarningMessage("No molecule on clipboard!");
				}
			}
		if (e.getActionCommand().equals(ITEM_CLEAR) && mIsEditable) {
			mMol.clear();
			mDisplayMol = mMol;
			structureChanged();
			}
		if (e.getActionCommand().equals(ITEM_OPEN_MOLFILE) && mIsEditable) {
			Component c = getParent();
			while (c != null && !(c instanceof Frame))
				c = c.getParent();
			File file = new FileHelper(c).selectFileToOpen("Save Structure", CompoundFileHelper.cFileTypeMOL, null);
			StereoMolecule mol = new MolfileParser().getCompactMolecule(file);
			if (mol != null) {
				if (!mAllowFragmentStatusChangeOnPasteOrDrop)
					mol.setFragment(mMol.isFragment());
				mMol = mol;
				mDisplayMol = mol;
				structureChanged();
			}
			else {
				showWarningMessage("Couldn't read molecule!");
			}
		}
		if (e.getActionCommand().equals(ITEM_SAVE_MOLFILE_V2)
		 || e.getActionCommand().equals(ITEM_SAVE_MOLFILE_V3)) {
			Component c = getParent();
			while (c != null && !(c instanceof Frame))
				c = c.getParent();
			String name = mMol.getName();
			if (name == null || name.isEmpty())
				name = "Unknown Molecule";
			String filename = new FileHelper(c).selectFileToSave("Save Structure", CompoundFileHelper.cFileTypeMOL, name);
			if (filename != null) {
				try {
					String molfile = e.getActionCommand().equals(ITEM_SAVE_MOLFILE_V2) ?
							  new MolfileCreator(mMol, false).getMolfile()
							: new MolfileV3Creator(mMol, false).getMolfile();
					BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename),"UTF-8"));
					writer.write(molfile);
					writer.close();
				}
				catch (IOException ioe) {
					showWarningMessage("Couldn't save molfile: "+ioe.getMessage());
					}
				}
			}
		}

	protected void showWarningMessage(String msg) {
		mWarningMessage = msg;
		repaint();
		new Thread(() -> {
			try { Thread.sleep(WARNING_MILLIS); } catch (InterruptedException ie) {}
			mWarningMessage = null;
			repaint();
			} ).start();
		}

	private void updateBackground() {
		Color bg = UIManager.getColor(isEditable() && isEnabled() ? "TextField.background" : "TextField.inactiveBackground");
		if (bg != null)
			setBackground(bg);
		}

	private boolean handlePopupTrigger(MouseEvent e) {
		if (!e.isPopupTrigger())
			return false;

		if (mMol != null) {
			JPopupMenu popup = new JPopupMenu();

			if (mClipboardHandler != null) {
				JMenuItem item1 = new JMenuItem(ITEM_COPY);
				item1.addActionListener(this);
				item1.setEnabled(mMol.getAllAtoms() != 0);
				popup.add(item1);

				JMenuItem itemCopySmiles = new JMenuItem(ITEM_COPY_SMILES);
				itemCopySmiles.addActionListener(this);
				itemCopySmiles.setEnabled(mMol.getAllAtoms() != 0);
				popup.add(itemCopySmiles);

				if (mIsEditable) {
					String itemText = StructureNameResolver.getInstance() == null ? ITEM_PASTE : ITEM_PASTE_WITH_NAME;
					JMenuItem item2 = new JMenuItem(itemText);
					item2.addActionListener(this);
					popup.add(item2);
					}

				popup.addSeparator();
				}

			if (mIsEditable) {
				JMenuItem item3 = new JMenuItem(ITEM_CLEAR);
				item3.addActionListener(this);
				item3.setEnabled(mMol.getAllAtoms() != 0);
				popup.add(item3);

				popup.addSeparator();

				JMenuItem itemOpenMolfile = new JMenuItem(ITEM_OPEN_MOLFILE);
				itemOpenMolfile.addActionListener(this);
				popup.add(itemOpenMolfile);

				popup.addSeparator();
				}

			JMenuItem itemSaveMolfileV2 = new JMenuItem(ITEM_SAVE_MOLFILE_V2);
			itemSaveMolfileV2.addActionListener(this);
			itemSaveMolfileV2.setEnabled(mMol.getAllAtoms() != 0);
			popup.add(itemSaveMolfileV2);

			JMenuItem itemSaveMolfileV3 = new JMenuItem(ITEM_SAVE_MOLFILE_V3);
			itemSaveMolfileV3.addActionListener(this);
			itemSaveMolfileV3.setEnabled(mMol.getAllAtoms() != 0);
			popup.add(itemSaveMolfileV3);

			popup.show(this, e.getX(), e.getY());
			}

		return true;
		}

	private void informListeners() {
		if (mListener != null)
			for (int i = 0; i<mListener.size(); i++)
				mListener.get(i).structureChanged(mMol);
		}

	private void initializeDragAndDrop(int dragAction, int dropAction) {
		final JStructureView outer = this;
		mAllowedDragAction = dragAction;
		mAllowedDropAction = dropAction;
		mAllowFragmentStatusChangeOnPasteOrDrop = false;

		if(dragAction != DnDConstants.ACTION_NONE){
			new MoleculeDragAdapter(this) {
				public Transferable getTransferable(Point origin) {
					return getMoleculeTransferable(origin);
				}

				public void onDragEnter() {
					outer.onDragEnter();
				}

				public void dragIsValidAndStarts() {
					mIsDraggingThis = true;
					}

				/*	public void onDragOver() {
					 outer.onDragOver();
					 }
				 */
				public void onDragExit() {
					outer.onDragExit();
				}

				public void dragDropEnd(DragSourceDropEvent e) {
					mIsDraggingThis = false;
				}
			};
		}

		if(dropAction != DnDConstants.ACTION_NONE) {
			mDropAdapter = new MoleculeDropAdapter() {
				public void onDropMolecule(StereoMolecule m,Point pt) {
					if (m != null && canDrop()){
						boolean isFragment = mMol.isFragment();
						mMol = new StereoMolecule(m);
				        mMol.removeAtomColors();
				        mMol.removeBondHiliting();
				        if (!mAllowFragmentStatusChangeOnPasteOrDrop)
				        	mMol.setFragment(isFragment);
				        mDisplayMol = mMol;
						repaint();
						informListeners();
						onDrop();
					}
					updateBorder(false);
				}

				public void dragEnter(DropTargetDragEvent e) {
					boolean drop = canDrop() && isDropOK(e) ;
					if (!drop)
						e.rejectDrag();
//					updateBorder(drop);
				}

				public void dragExit(DropTargetEvent e) {
//					updateBorder(false);
				}
			};

			new DropTarget(this, mAllowedDropAction, mDropAdapter, true);
//			new DropTarget(this,mAllowedDropAction,mDropAdapter,true, getSystemFlavorMap());
		}
	}


	protected Transferable getMoleculeTransferable(Point pt) {
		return new MoleculeTransferable(mMol);
	}

	// Drag notifications if needed by subclasses
	protected void onDragEnter() {}
	protected void onDragExit() {}
	protected void onDragOver() {}
	protected void onDrop() {}

	private void updateBorder(boolean showBorder) {
		if (mShowBorder != showBorder) {
			mShowBorder = showBorder;
			repaint();
			}
		}

/*	public java.awt.datatransfer.FlavorMap getSystemFlavorMap() {
	    return new OurFlavorMap();
	    }

    // This class is needed for inter-jvm drag&drop. Although not neccessary for standard environments, it prevents
    // nasty "no native data was transfered" errors. It still might create ClassNotFoundException in the first place by
    // the SystemFlavorMap, but as I found it does not hurt, since the context classloader will be installed after
    // the first call. I know, that this depends heavely on a specific behaviour of the systemflavormap, but for now
    // there's nothing I can do about it.
    static class OurFlavorMap implements FlavorMap, FlavorTable {
    	public java.util.Map<DataFlavor,String> getNativesForFlavors(DataFlavor[] dfs) {
    //	    System.out.println("getNativesForFlavors " + dfs.length);
    //	    for (int i = 0; i < dfs.length; i++)
    //		    System.out.println(" -> " + dfs[i]);
    //
    	    return SystemFlavorMap.getDefaultFlavorMap().getNativesForFlavors(dfs);
    	    }
    
    	public java.util.Map<String,DataFlavor> getFlavorsForNatives(String[] natives) {
    //	    System.out.println("getFlavorsForNatives " + natives.length);
    //	    for (int i = 0; i < natives.length; i++)
    //	        System.out.println(" -> " + natives[i]);
    //
    	    return SystemFlavorMap.getDefaultFlavorMap().getFlavorsForNatives(natives);
    	    }
    
    	public synchronized java.util.List<DataFlavor> getFlavorsForNative(String nat) {
    //	    System.out.println("getFlavorsForNative " + nat);
    	    
    	    return ((SystemFlavorMap)SystemFlavorMap.getDefaultFlavorMap()).getFlavorsForNative(nat);
    	    }
    
    	public synchronized java.util.List<String> getNativesForFlavor(DataFlavor flav) {
    //	    System.out.println("getNativesForFlavor " + flav);
    	    return ((SystemFlavorMap)SystemFlavorMap.getDefaultFlavorMap()).getNativesForFlavor(flav);
    	    }
        }*/
    }
