package ciss.phase_viewer.plugins.viewer.edit;

import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Vector;

import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.border.TitledBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.filechooser.FileFilter;

import org.apache.log4j.Logger;
import org.jdom.Attribute;
import org.jdom.DataConversionException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

import ciss.phase_viewer.acviewer.ACVAction;
import ciss.phase_viewer.acviewer.ACVCaller;
import ciss.phase_viewer.acviewer.ACVData;
import ciss.phase_viewer.acviewer.ConfigData;
import ciss.phase_viewer.acviewer.ConfigDataUpdateEvent;
import ciss.phase_viewer.acviewer.CoordsViewerInterface;
import ciss.phase_viewer.acviewer.J3DPanel;
import ciss.phase_viewer.acviewer.MainPanel;
import ciss.phase_viewer.atomcoord.Atom;
import ciss.phase_viewer.atomcoord.AtomCoords;
import ciss.phase_viewer.atomcoord.Cell;
import ciss.phase_viewer.atomcoord.io.CIFExporter;
import ciss.phase_viewer.atomcoord.io.CIFImporter;
import ciss.phase_viewer.atomcoord.pmodel.PmodelException;
import ciss.phase_viewer.common.ChaseFileChooser;
import ciss.phase_viewer.common.CoordsViewerBooter;
import ciss.phase_viewer.common.StringConstants;
import ciss.phase_viewer.inputinterface.DataManager;
import ciss.phase_viewer.inputinterface.InputInterface;
import ciss.phase_viewer.inputinterface.InputInterfacePrimitiveEntry;
import ciss.phase_viewer.inputinterface.InputInterfaceUnits;
import ciss.phase_viewer.mainpanel.Desk;
import ciss.phase_viewer.mainpanel.InternalFrameChase;
import ciss.phase_viewer.plugins.projectmanipulator.phase.PhaseConstants;
import ciss.phase_viewer.plugins.viewer.edit.Interface.INITIAL_AB;
import ciss.phase_viewer.plugins.viewer.edit.Interface.INITIAL_COORD;
import ciss.phase_viewer.primitiveguis.ValueSlider;
import ciss.phase_viewer.primitiveguis.ValueSliderListener;

public class InterfaceAction extends ACVAction {

	public InterfaceAction(J3DPanel j3dPanel) {
		super(j3dPanel);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		new InterfaceInitializer((MainPanel) parent);
	}

}

class InterfaceInitializer extends InternalFrameChase implements ACVCaller {
	private MainPanel parent;
	private Logger logger = Logger.getLogger(InterfaceInitializer.class.getName());

	InterfaceInitializer(MainPanel parent) {
		super("initialize interface", new java.awt.Dimension(420, 380));
		this.parent = parent;
		this.parent.addDisposeOnExit(this);
		init();
	}

	private FileSelector pscif1;
	private FileSelector pscif2;
	private FileSelector pspvm;
	private JPanel pps;
	private JRadioButton jrcif;
	private JRadioButton jrpvm;
	private JButton btnok;
	private JComboBox<String> comboAB;
	private JComboBox<String> comboCoord1;

	private void init() {
		ButtonGroup bg = new ButtonGroup();
		jrcif = new JRadioButton("CIF");
		jrpvm = new JRadioButton("PHASE-Viewer model file");
		bg.add(jrcif);
		bg.add(jrpvm);
		JPanel pbg = new JPanel();
		pbg.setBorder(new TitledBorder("select File type"));
		pbg.add(jrcif);
		pbg.add(jrpvm);
		JPanel pp = new JPanel();
		pp.setLayout(new BoxLayout(pp, BoxLayout.Y_AXIS));
		pps = new JPanel();
		pscif1 = new FileSelector("CIF for crystal1");
		pscif2 = new FileSelector("CIF for crystal2");
		pspvm = new FileSelector("PHASE-Viewer model file", false);
		pp.add(pbg);
		pp.add(pps);
		comboAB = new JComboBox<>(new String[] { "average", "crystal1", "crystal2" });
		comboCoord1 = new JComboBox<>(new String[] { "internal", "cartesian" });
		JPanel pab = new JPanel();
		pab.setBorder(new TitledBorder("a-axis & b-axis"));
		JPanel pc1 = new JPanel();
		pc1.setBorder(new TitledBorder("coordinates"));
		pab.add(comboAB);
		pc1.add(comboCoord1);
		JPanel popts = new JPanel();
		popts.setBorder(new TitledBorder("options"));
		popts.setLayout(new BoxLayout(popts, BoxLayout.X_AXIS));
		popts.add(pab);
		popts.add(pc1);
		pp.add(popts);
		JPanel pbtn = new JPanel();
		btnok = new JButton("ok");
		btnok.setEnabled(false);
		JButton btncan = new JButton("cancel");
		pbtn.setLayout(new BoxLayout(pbtn, BoxLayout.X_AXIS));
		pbtn.add(btnok);
		pbtn.add(btncan);
		pp.add(pbtn);

		getContentPane().add(pp);

		pscif1.addCaretListener(new CaretListener() {
			@Override
			public void caretUpdate(CaretEvent e) {
				okBtn();
			}
		});

		pscif2.addCaretListener(new CaretListener() {
			@Override
			public void caretUpdate(CaretEvent e) {
				okBtn();
			}
		});

		pspvm.addCaretListener(new CaretListener() {
			@Override
			public void caretUpdate(CaretEvent e) {
				okBtn();
			}
		});

		jrcif.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				swapUI();
				okBtn();
			}
		});

		jrpvm.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				swapUI();
				okBtn();
			}
		});

		btnok.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				genInterface();
			}
		});

		btncan.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				dispose();
			}
		});

		jrcif.setSelected(true);
		pps.add(pscif1);
		pps.add(pscif2);
		revalidate();
	}

	private void okBtn() {
		if (jrcif.isSelected()) {
			boolean b1 = pscif1.pathExists();
			boolean b2 = pscif2.pathExists();
			btnok.setEnabled(b1 && b2);
		} else {
			boolean b1 = pspvm.pathExists();
			btnok.setEnabled(b1);
		}
	}

	private void genInterface() {
		dispose();
		Interface inter = null;
		if (jrcif.isSelected()) {
			logger.info("building interface model from two CIF");
			File f1 = new File(pscif1.getPath());
			File f2 = new File(pscif2.getPath());
			INITIAL_AB initialAB = INITIAL_AB.AVERAGE;
			if (comboAB.getSelectedIndex() == 1)
				initialAB = INITIAL_AB.CRYSTAL1;
			if (comboAB.getSelectedIndex() == 2)
				initialAB = INITIAL_AB.CRYSTAL2;
			INITIAL_COORD initialCoord = INITIAL_COORD.FRACTIONAL;
			if (comboCoord1.getSelectedIndex() == 1)
				initialCoord = INITIAL_COORD.CARTESIAN;
			try {
				inter = new Interface(f1, f2, initialAB, initialCoord);
			} catch (Exception e) {
				e.printStackTrace();
			}
		} else {
			logger.info("building interface model from the PVM file");
			File f1 = new File(pspvm.getPath());
			try {
				inter = new Interface(f1);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

		if (inter != null) {
			try {
				AtomCoords coords = inter.getAtomCoordsObj();
				Vector<AtomCoords> vec = new Vector<>();
				vec.addElement(coords);
				DataManager dm = new DataManager("foo", PhaseConstants.TABLESPEC, false);
				dm.parse();
				ACVData data = new ACVData(this, vec, "", dm.getInputInterface(), StringConstants.phase_atom_tag);
				((CoordsViewerInterface) parent).setData(data);
				((CoordsViewerInterface) parent).display3D();
				parent.setSelected(true);
				new InterfaceBuilder(parent, inter);
			} catch (Exception e) {
				dispose();
				e.printStackTrace();
				logger.error("failed to initialize interface model");
				return;
			}
		} else {
			JOptionPane.showInternalMessageDialog(Desk.getDesktop().getSelectedFrame(),
					"failed to create initial interface structure");
			logger.error("failed to create initial interface structure");
		}
	}

	private void swapUI() {
		if (jrcif.isSelected()) {
			pps.remove(pspvm);
			pps.revalidate();
			pps.add(pscif1);
			pps.add(pscif2);
		}
		if (jrpvm.isSelected()) {
			pps.remove(pscif1);
			pps.remove(pscif2);
			pps.revalidate();
			pps.add(pspvm);
		}
		comboAB.setEnabled(jrcif.isSelected());
		comboCoord1.setEnabled(jrcif.isSelected());
		repaint();
	}

	@Override
	public void save(AtomCoords coords) {
	}
}

class FileSelector extends JPanel implements PropertyChangeListener, ACVCaller {
	private JTextField tfpath;
	private JButton btnchooser;
	private JButton btnV;
	private String title;
	private int width = 20;
	private boolean btnView;

	FileSelector(String title) {
		this(title, true);
	}

	FileSelector(String title, boolean btnView) {
		this.title = title;
		this.btnView = btnView;
		init();
	}

	void addCaretListener(CaretListener cl) {
		tfpath.addCaretListener(cl);
	}

	private void init() {
		this.setBorder(new TitledBorder(title));
		this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
		if (tfpath == null) {
			tfpath = new JTextField(width);
		}
		this.add(tfpath);
		btnchooser = new JButton("choose...");
		this.add(btnchooser);
		if (btnView) {
			btnV = new JButton("viewer");
			btnV.setEnabled(false);
			btnV.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					bootViewer();
				}
			});
			this.add(btnV);
		}
		int x = btnchooser.getSize().width;
		int y = btnchooser.getSize().height;
		tfpath.setPreferredSize(new Dimension(x, y));

		btnchooser.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				bootFileChooser();
			}
		});
		if (btnView)
			tfpath.addCaretListener(new CaretListener() {
				@Override
				public void caretUpdate(CaretEvent e) {
					btnV.setEnabled(pathExists());
				}
			});
	}

	private void bootViewer() {
		CIFImporter cifimp = new CIFImporter();
		cifimp.createOptionsPanel(false, this, getPath());
		AtomCoords[] c1 = cifimp.getAtomCoordsFrom(getPath());
		if (c1 == null) {
			JOptionPane.showInternalMessageDialog(Desk.getDesktop().getSelectedFrame(),
					"failed to import atomic coordinatews from " + getPath());
			return;
		}
		Vector<AtomCoords> cv = new Vector<>();
		for (AtomCoords c : c1)
			cv.add(c);
		DataManager dm = new DataManager("foo", PhaseConstants.TABLESPEC, false);
		ACVData acvdata = new ACVData(this, cv, getPath(), dm.getInputInterface(), StringConstants.phase_atom_tag);
		CoordsViewerBooter cvb = new CoordsViewerBooter(acvdata);
		try {
			cvb.boot();
		} catch (Exception e) {
			e.printStackTrace();
			JOptionPane.showInternalMessageDialog(Desk.getDesktop().getSelectedFrame(), "failed to boot viewer");
		}
	}

	private void bootFileChooser() {
		ChaseFileChooser fdlg = new ChaseFileChooser(ChaseFileChooser.atom, true);
		fdlg.setDialogType(JFileChooser.CUSTOM_DIALOG);
		fdlg.setDialogTitle("choose");

		fdlg.setFileSelectionMode(JFileChooser.FILES_ONLY);

		if (fdlg.showDialog(this, "Select") != JFileChooser.APPROVE_OPTION) {
			return;
		}
		tfpath.setText(fdlg.getSelectedFile().getAbsolutePath());
	}

	boolean pathExists() {
		return new File(tfpath.getText()).exists();
	}

	String getPath() {
		return tfpath.getText();
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
	}

	@Override
	public void save(AtomCoords coords) {
	}
}

class InterfaceBuilder extends InternalFrameChase implements ConfigData {
	private Logger logger = Logger.getLogger(InterfaceBuilder.class.getName());
	private MainPanel parent;
	private Interface inter;
	private JTextField[][] tflatvec;
	private JCheckBox cbnonperiodic;
	private JCheckBox cbnonperiodic_trans0, cbnonperiodic_trans1;
	private ValueSlider vstransa1, vstransb1, vstransc1, vsnonpc1;
	private ValueSlider vstransa2, vstransb2, vstransc2, vsnonpc2;
	private ValueSlider vstransa0, vstransb0, vstransc0;
	private JToggleButton btninv1, btninv2;

	private DecimalFormat df = new DecimalFormat("0.########");

	InterfaceBuilder(MainPanel parent, Interface inter) {
		super("interface builder", new java.awt.Dimension(800, 640));
		this.parent = parent;
		this.parent.addDisposeOnExit(this);
		this.inter = inter;
		this.parent.getCD().register(this);
		init();
	}

	private void init() {
		try {
			JPanel p = new JPanel();
			p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
			p.add(latticePanel());
			p.add(periodicTranslationPanel(1));
			p.add(periodicTranslationPanel(0));
			p.add(periodicTranslationPanel(-1));
			p.add(invertPanel());
			p.add(btnPanel());
			getContentPane().add(p);
			updateInterfaceUI();
			repaint();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private int tfsizex = 20;
	private int tfsizey = 10;

	private JPanel latticePanel() {
		JPanel ret = new JPanel();
		ret.setBorder(new TitledBorder("lattice vector"));
		ret.setLayout(new BoxLayout(ret, BoxLayout.Y_AXIS));
		cbnonperiodic = new JCheckBox("c-axis non-periodic");
		ret.add(cbnonperiodic);
		JPanel lat = new JPanel();
		lat.setLayout(new GridLayout(3, 4));
		double[][] latvec = inter.getLatVec();
		tflatvec = new JTextField[3][3];
		String[] vecname = { "a-axis", "b-axis", "c-axis" };
		ActionListener al = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				updateConfiguration();
			}
		};
		for (int i = 0; i < 3; i++) {
			int ii = 2 - i;
			lat.add(new JLabel(vecname[ii]), ii, 0);
			for (int j = 0; j < 3; j++) {
				tflatvec[ii][j] = new JTextField(10);
				tflatvec[ii][j].setPreferredSize(new Dimension(tfsizex, tfsizey));
				tflatvec[ii][j].setText(df.format(latvec[ii][j]));
				tflatvec[ii][j].addActionListener(al);
				lat.add(tflatvec[ii][j], ii, j + 1);
			}
		}
		ret.add(lat);
		return ret;
	}

	private JPanel pTransPanel1;
	private JPanel pTransPanel2;

	private void setPeriodicTranslationPanel(int cid) {
		JPanel p = null;
		ValueSlider va = null;
		ValueSlider vb = null;
		ValueSlider vc = null;
		if (cid == 0) {
			p = pTransPanel1;
			va = vstransa1;
			vb = vstransb1;
			vc = vstransc1;
			if (cbnonperiodic_trans0.isSelected())
				vc = vsnonpc1;
		} else if (cid == 1) {
			p = pTransPanel2;
			va = vstransa2;
			vb = vstransb2;
			vc = vstransc2;
			if (cbnonperiodic_trans1.isSelected())
				vc = vsnonpc2;
		}
		p.removeAll();
		p.setLayout(new GridLayout(1, 3));
		p.add(va);
		p.add(vb);
		p.add(vc);
		p.revalidate();
		p.repaint();
	}

	private JPanel periodicTranslationPanel(int cid) {
		JPanel ret = new JPanel();
		if (cid == 0)
			pTransPanel1 = ret;
		else if (cid == 1)
			pTransPanel2 = ret;
		double[] latconst = inter.getLatConst();
		ValueSlider vsa = new ValueSlider(0f, (float) latconst[0]);
		vsa.setTitleForBorder("a-axis");
		vsa.setValue(0f);

		ValueSlider vsb = new ValueSlider(0, (float) latconst[1]);
		vsb.setTitleForBorder("b-axis");
		vsb.setValue(0f);

		float z = (float) inter.getCrystal1().getLatConst()[2];
		if (cid == 1)
			z = (float) inter.getCrystal2().getLatConst()[2];
		if (cid == -1)
			z = (float) inter.getLatConst()[2];
		ValueSlider vsc = new ValueSlider(0f, z);
		vsc.setTitleForBorder("c-axis");
		vsc.setValue(0f);

		float zz = (float) inter.getLatConst()[2];
		ValueSlider vscc = new ValueSlider(0f, zz);
		vscc.setTitleForBorder("c-axis");
		vscc.setValue(0f);

		ret.setLayout(new GridLayout(1, 3));
		ret.add(vsa);
		ret.add(vsb);
		ret.add(vsc);

		ValueSliderListener vsl = new ValueSliderListener() {
			@Override
			public void valueSliderValueChanged() {
				updateConfiguration();
			}
		};
		vsa.addValueSliderListener(vsl);
		vsb.addValueSliderListener(vsl);
		vsc.addValueSliderListener(vsl);
		vscc.addValueSliderListener(vsl);

		if (cid == 0) {
			vstransa1 = vsa;
			vstransb1 = vsb;
			vstransc1 = vsc;
			vsnonpc1 = vscc;
		}
		if (cid == 1) {
			vstransa2 = vsa;
			vstransb2 = vsb;
			vstransc2 = vsc;
			vsnonpc2 = vscc;
		}
		if (cid == -1) {
			vstransa0 = vsa;
			vstransb0 = vsb;
			vstransc0 = vsc;
		}
		JCheckBox cb = null;
		if (cid >= 0) {
			cb = new JCheckBox("c-axis non-periodic");
			cbnonperiodic_trans0 = cb;
			if (cid == 1)
				cbnonperiodic_trans1 = cb;
		}
		JPanel rret = new JPanel();
		if (cid == 0)
			rret.setBorder(new TitledBorder("periodic translation for crystal 1"));
		else if (cid == 1)
			rret.setBorder(new TitledBorder("periodic translation for crystal 2"));
		else if (cid == -1)
			rret.setBorder(new TitledBorder("periodic translation for the whole system"));
		rret.setLayout(new BoxLayout(rret, BoxLayout.Y_AXIS));
		if (cid >= 0)
			rret.add(cb);
		rret.add(ret);

		if (cid >= 0)
			cb.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					setPeriodicTranslationPanel(cid);
				}
			});

		return rret;
	}

	private JPanel invertPanel() {
		JPanel ret = new JPanel();
		ret.setBorder(new TitledBorder("invert"));
		ret.setLayout(new GridLayout(1, 2));
		btninv1 = new JToggleButton("crystal 1");
		btninv2 = new JToggleButton("crystal 2");
		ret.add(btninv1);
		ret.add(btninv2);
		ActionListener al = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				updateConfiguration();
			}
		};
		btninv1.addActionListener(al);
		btninv2.addActionListener(al);
		return ret;
	}

	private JPanel btnPanel() {
		JPanel ret = new JPanel();
		ret.setLayout(new BoxLayout(ret, BoxLayout.X_AXIS));
		JButton expor = new JButton("export");
		JButton cls = new JButton("close");

		expor.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				doExport();
			}
		});

		cls.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				dispose();
			}
		});
		ret.add(expor);
		ret.add(cls);
		return ret;
	}

	private void doExport() {
		ChaseFileChooser cfc = new ChaseFileChooser(ChaseFileChooser.atom, true);
		cfc.setFileSelectionMode(JFileChooser.FILES_ONLY);
		FileFilter ciff = new FileFilter() {
			@Override
			public String getDescription() {
				return "CIF";
			}

			@Override
			public boolean accept(File f) {
				return true;
			}
		};
		FileFilter finp = new FileFilter() {
			@Override
			public String getDescription() {
				return "F_INP";
			}

			@Override
			public boolean accept(File f) {
				return true;
			}
		};
		FileFilter[] filters = cfc.getChoosableFileFilters();
		for (FileFilter filter : filters)
			cfc.removeChoosableFileFilter(filter);
		cfc.addChoosableFileFilter(ciff);
		cfc.addChoosableFileFilter(finp);
		if (cfc.showDialog(this, "Select") != JFileChooser.APPROVE_OPTION) {
			JOptionPane.showInternalMessageDialog(Desk.getDesktop().getSelectedFrame(), "export canceled");
			logger.info("operation canceled");
			return;
		}
		AtomCoords coor = inter.getAtomCoordsObj(true);
		if (cfc.getFileFilter() == ciff)
			cifExport(coor, cfc.getSelectedFile());
		else if (cfc.getFileFilter() == finp)
			F_INPExport(coor, cfc.getSelectedFile());
		PVMExport(cfc.getSelectedFile());
	}

	private void cifExport(AtomCoords coor, File file) {
		CIFExporter ce = new CIFExporter();
		ce.writeAtomCoordsTo(new AtomCoords[] { coor }, file.getAbsolutePath());
		logger.info("output interface coordinates to " + file.getAbsolutePath());
	}

	private void F_INPExport(AtomCoords coor, File file) {
		try {
			coor.convert(AtomCoords.TO_INTERNAL, AtomCoords.TO_BOHR);
			DataManager manager = new DataManager(file.getAbsolutePath(), PhaseConstants.TABLESPEC, false);
			manager.parse();
			InputInterface inp = manager.getInputInterface();
			InputInterfaceUnits uni = inp.getInputInterfaceUnits("structure.atom_list.atoms.units");
			uni.setUnits(new String[] { "bohr" });
			uni = inp.getInputInterfaceUnits("structure.unit_cell.units");
			uni.setUnits(new String[] { "bohr" });
			InputInterfacePrimitiveEntry coorsys = inp
					.getInputInterfacePrimitiveEntry("structure.atom_list.coordinate_system");
			coorsys.setValue("");
			coor.atomCoords2InputInterface(inp, StringConstants.phase_atom_tag);
			coor.cell2InputInterface(inp);
			manager.getInputInterface().saveTo(file);
			logger.info("output interface coordinates to " + file.getAbsolutePath());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	private void PVMExport(File file) {
		Document doc = new Document();
		Element theRoot = new Element("PhaseViewerModel");
		doc.setRootElement(theRoot);
		Element crystal1 = inter.getCrystal1().getElement();
		Element crystal2 = inter.getCrystal2().getElement();
		Element interelem = inter.getElement();
		theRoot.addContent(crystal1);
		theRoot.addContent(crystal2);
		theRoot.addContent(interelem);

		String fname = file.getName();
		String pvmFile = "";
		String[] aa = fname.split("\\.");
		if (aa.length == 1)
			pvmFile = aa + ".pvm";
		for (int i = 0; i < aa.length - 1; i++)
			pvmFile += aa[i] + ".";
		pvmFile = pvmFile.substring(0, pvmFile.length() - 1) + ".pvm";
		File f = new File(file.getParent() + System.getProperty("file.separator") + pvmFile);
		saveXML(doc, f);
		logger.info("output aux interface info to    " + f.getAbsolutePath());
	}

	public boolean saveXML(Document doc, File file) {
		XMLOutputter xmlOutput = new XMLOutputter();
		xmlOutput.setFormat(Format.getPrettyFormat());
		OutputStreamWriter osw = null;
		FileOutputStream fos = null;
		try {
			fos = new FileOutputStream(file);
			osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
			xmlOutput.output(doc.getDocument(), osw);
		} catch (IOException e1) {
			logger.error("failed to write information to " + file);
			return false;
		} finally {
			try {
				fos.close();
				osw.close();
			} catch (IOException ioe) {
			}
		}
		return true;
	}

	private void updateConfiguration() {
		double[][] latvec = inter.getLatVec();
		boolean latchanged = false;
		for (int i = 0; i < 3; i++)
			for (int j = 0; j < 3; j++) {
				try {
					double val = Double.parseDouble(tflatvec[i][j].getText().trim());
					if (Math.abs(latvec[i][j] - val) > 1e-5) {
						latvec[i][j] = val;
						latchanged = true;
					}
				} catch (NumberFormatException nfe) {
				}
			}
		if (latchanged) {
			inter.setLatticeVector(latvec);
			inter.updateLatticeConstant();
			inter.updateCart(0);
			inter.updateCart(1);
			if (cbnonperiodic.isSelected())
				inter.updateFract();
			else
				inter.updateCart(2);
		}

		double vala = vstransa1.getValue();
		double valb = vstransb1.getValue();
		double valc = vstransc1.getValue();
		if (cbnonperiodic_trans0.isSelected())
			valc = vsnonpc1.getValue();
		boolean trans = inter.doPeriodicTranslation(0, vala, valb, valc, cbnonperiodic_trans0.isSelected());
		vala = vstransa2.getValue();
		valb = vstransb2.getValue();
		valc = vstransc2.getValue();
		if (cbnonperiodic_trans1.isSelected())
			valc = vsnonpc2.getValue();
		trans = inter.doPeriodicTranslation(1, vala, valb, valc, cbnonperiodic_trans1.isSelected()) || trans;

		vala = vstransa0.getValue();
		valb = vstransb0.getValue();
		valc = vstransc0.getValue();
		trans = inter.doPeriodicTranslationInterface(vala, valb, valc) || trans;

		boolean inver = inter.invert(0, btninv1.isSelected());
		inver = inter.invert(1, btninv2.isSelected()) || inver;

		if (trans || latchanged || inver) {
			AtomCoords coor = parent.getCD().getAtomCoords();
			coor.saveState();
			boolean wasFract = coor.isInternal();
			int nat = coor.getAtomList().getNumAt();
			Atom atmt = coor.getAtomList().getAtomAt(0);
			coor.setCell(new Cell(latvec[0], latvec[1], latvec[2]));

			coor.convertCell(Cell.CELL_ABC);
			for (int i = 0; i < inter.getNumAt(); i++) {
				Atom atm = atmt.getCopy();
				atm.setElementName(inter.getElementName()[i]);
				atm.setPos(inter.getCartPos()[i]);
				if (i < nat)
					coor.getAtomList().replaceAtomAt(i, atm);
				else
					coor.getAtomList().addAtom(atm);
			}
			int undo = inter.getNumAt() + 1;
			if (wasFract) {
				coor.convert(AtomCoords.TO_INTERNAL, AtomCoords.TO_ANG);
				undo += inter.getNumAt();
			}
			coor.getAtomList().setUndoCount(undo);
			parent.getCD().setCoords(coor);
			coor.finalizeState();
		}
	}

	@Override
	public void configDataUpdate() {
	}

	@Override
	public void configDataUpdate(boolean rescaleOnUpdate, ConfigDataUpdateEvent e) {
		AtomCoords coor = parent.getCD().getAtomCoords();
		inter.mergeAtomCoords(coor);
		updateInterfaceUI();
	}

	private void updateInterfaceUI() {
		double[][] latv = inter.getLatVec();
		boolean b1 = inter.isInverted(0);
		boolean b2 = inter.isInverted(1);
		double[] off0 = inter.getOffset(-1);
		double[] off1 = inter.getOffset(0);
		double[] off2 = inter.getOffset(1);
		double[] noff1 = inter.getNonPeriodicOffset(0);
		double[] noff2 = inter.getNonPeriodicOffset(1);
		double[] lconst = inter.getLatConst();
		double a = lconst[0];
		double b = lconst[1];
		double c = lconst[2];
		double cc1 = inter.getCrystal1().getLatConst()[2];
		double cc2 = inter.getCrystal2().getLatConst()[2];
		for (int i = 0; i < 3; i++)
			for (int j = 0; j < 3; j++)
				tflatvec[i][j].setText(String.valueOf(latv[i][j]));
		setSelectedQuietly(btninv1, b1);
		setSelectedQuietly(btninv2, b2);
		setMinMaxValQuietly(vsnonpc1, 0f, (float) c, (float) noff1[2]);
		setMinMaxValQuietly(vsnonpc2, 0f, (float) c, (float) noff2[2]);
		setMinMaxValQuietly(vstransa1, 0f, (float) a, (float) off1[0]);
		setMinMaxValQuietly(vstransb1, 0f, (float) b, (float) off1[1]);
		setMinMaxValQuietly(vstransc1, 0f, (float) cc1, (float) off1[2]);
		setMinMaxValQuietly(vstransa2, 0f, (float) a, (float) off2[0]);
		setMinMaxValQuietly(vstransb2, 0f, (float) b, (float) off2[1]);
		setMinMaxValQuietly(vstransc2, 0f, (float) cc2, (float) off2[2]);
		setMinMaxValQuietly(vstransa0, 0f, (float) a, (float) off0[0]);
		setMinMaxValQuietly(vstransb0, 0f, (float) b, (float) off0[1]);
		setMinMaxValQuietly(vstransc0, 0f, (float) c, (float) off0[2]);
	}

	private void setSelectedQuietly(JToggleButton btn, boolean val) {
		ActionListener[] ls = btn.getActionListeners();
		for (ActionListener l : ls)
			btn.removeActionListener(l);
		btn.setSelected(val);
		for (ActionListener l : ls)
			btn.addActionListener(l);
	}

	private void setMinMaxValQuietly(ValueSlider vs, float min, float max, float val) {
		float[] minmax = vs.getMinMax();
		ValueSliderListener[] listeners = vs.getValueSliderListener();
		if (listeners != null)
			for (ValueSliderListener listener : listeners)
				vs.removeValueSliderListener(listener);
		if (Math.abs(minmax[0] - min) > 1e-5 || Math.abs(minmax[1] - max) > 1e-5)
			vs.setMinMax(min, max);
		vs.setValue(val);
		if (listeners != null)
			for (ValueSliderListener listener : listeners)
				vs.addValueSliderListener(listener);
	}

	@Override
	public boolean needsUpdate() {
		return true;
	}
}

class Interface extends AtomicConfiguration implements PropertyChangeListener {
	enum INITIAL_AB {
		AVERAGE, CRYSTAL1, CRYSTAL2
	};

	enum INITIAL_COORD {
		FRACTIONAL, CARTESIAN
	};

	private Logger logger = Logger.getLogger(Interface.class.getName());

	private File crystal1File;
	private File crystal2File;

	private AtomicConfiguration crystal1;
	private AtomicConfiguration crystal2;
	private int[][] periodicTranslation = null;
	private boolean binv1, binv2;
	private double[] zoff1 = { 0d, 0d, 0d }, zoff2 = { 0d, 0d, 0d };

	private double[] offset0 = { 0d, 0d, 0d };
	private double[] offset1 = { 0d, 0d, 0d };
	private double[] offset2 = { 0d, 0d, 0d };

	private double[] offset1_nonp = { 0d, 0d, 0d };
	private double[] offset2_nonp = { 0d, 0d, 0d };
	private INITIAL_AB initialAB;
	private INITIAL_COORD initialCoord;

	protected Interface(File pvmFile) throws IOException {
		SAXBuilder builder = new SAXBuilder();
		Document doc = null;
		try {
			doc = builder.build(pvmFile);
		} catch (JDOMException e) {
			throw new IOException("Failed to parse " + pvmFile.getAbsolutePath());
		}
		Element rootElement = doc.getRootElement();
		List<Element> elements = rootElement.getChildren("AtomicConfiguration");
		for (Element element : elements) {
			Attribute nam = element.getAttribute("name");
			if (nam == null)
				continue;
			if (nam.getValue().equalsIgnoreCase("crystal1")) {
				crystal1 = new AtomicConfiguration();
				crystal1.initFromElement(element);
			}
			if (nam.getValue().equalsIgnoreCase("crystal2")) {
				crystal2 = new AtomicConfiguration();
				crystal2.initFromElement(element);
			}
			if (nam.getValue().equalsIgnoreCase("interface")) {
				initFromElement(element);
			}
		}
	}

	protected Interface(File crystal1File, File crystal2File, INITIAL_AB initialAB, INITIAL_COORD initialCoord)
			throws IOException, PmodelException {
		this.crystal1File = crystal1File;
		this.crystal2File = crystal2File;
		this.initialAB = initialAB;
		this.initialCoord = initialCoord;
		logger.info("initialCoord " + initialCoord);
		if (!this.crystal1File.exists())
			throw new IOException("crystal1 file does not exist");
		if (!this.crystal2File.exists())
			throw new IOException("crystal1 file does not exist");
		init();
	}

	Element getElement() {
		Element ret = super.getElement();
		Element interfaceElement = new Element("interface");
		ret.addContent(interfaceElement);
		Element inv1 = new Element("inv1");
		inv1.setAttribute("val", String.valueOf(binv1));
		Element inv2 = new Element("inv2");
		inv2.setAttribute("val", String.valueOf(binv2));
		Element off0 = new Element("off0");
		off0.setAttribute("x", String.valueOf(offset0[0]));
		off0.setAttribute("y", String.valueOf(offset0[1]));
		off0.setAttribute("z", String.valueOf(offset0[2]));
		Element off1 = new Element("off1");
		off1.setAttribute("x", String.valueOf(offset1[0]));
		off1.setAttribute("y", String.valueOf(offset1[1]));
		off1.setAttribute("z", String.valueOf(offset1[2]));
		Element off2 = new Element("off2");
		off2.setAttribute("x", String.valueOf(offset2[0]));
		off2.setAttribute("y", String.valueOf(offset2[1]));
		off2.setAttribute("z", String.valueOf(offset2[2]));
		Element zoff1 = new Element("zoff1");
		zoff1.setAttribute("x", String.valueOf(this.zoff1[0]));
		zoff1.setAttribute("y", String.valueOf(this.zoff1[1]));
		zoff1.setAttribute("z", String.valueOf(this.zoff1[2]));
		Element zoff2 = new Element("zoff2");
		zoff2.setAttribute("x", String.valueOf(this.zoff2[0]));
		zoff2.setAttribute("y", String.valueOf(this.zoff2[1]));
		zoff2.setAttribute("z", String.valueOf(this.zoff2[2]));
		Element off_nonp1 = new Element("off_nonp1");
		off_nonp1.setAttribute("x", String.valueOf(this.offset1_nonp[0]));
		off_nonp1.setAttribute("y", String.valueOf(this.offset1_nonp[1]));
		off_nonp1.setAttribute("z", String.valueOf(this.offset1_nonp[2]));
		Element off_nonp2 = new Element("off_nonp2");
		off_nonp2.setAttribute("x", String.valueOf(this.offset2_nonp[0]));
		off_nonp2.setAttribute("y", String.valueOf(this.offset2_nonp[1]));
		off_nonp2.setAttribute("z", String.valueOf(this.offset2_nonp[2]));
		interfaceElement.addContent(inv1);
		interfaceElement.addContent(inv2);
		interfaceElement.addContent(off0);
		interfaceElement.addContent(off1);
		interfaceElement.addContent(off2);
		interfaceElement.addContent(zoff1);
		interfaceElement.addContent(zoff2);
		interfaceElement.addContent(off_nonp1);
		interfaceElement.addContent(off_nonp2);
		return ret;
	}

	void initFromElement(Element element) {
		super.initFromElement(element);
		Element inter = element.getChild("interface");
		Element inv1 = inter.getChild("inv1");
		Element inv2 = inter.getChild("inv2");
		Element off0 = inter.getChild("off0");
		Element off1 = inter.getChild("off1");
		Element off2 = inter.getChild("off2");
		Element zoff1 = inter.getChild("zoff1");
		Element zoff2 = inter.getChild("zoff2");
		Element off_nonp1 = inter.getChild("off_nonp1");
		Element off_nonp2 = inter.getChild("off_nonp2");
		try {
			binv1 = inv1.getAttribute("val").getBooleanValue();
			binv2 = inv2.getAttribute("val").getBooleanValue();
			offset0[0] = off0.getAttribute("x").getDoubleValue();
			offset0[1] = off0.getAttribute("y").getDoubleValue();
			offset0[2] = off0.getAttribute("z").getDoubleValue();
			offset1[0] = off1.getAttribute("x").getDoubleValue();
			offset1[1] = off1.getAttribute("y").getDoubleValue();
			offset1[2] = off1.getAttribute("z").getDoubleValue();
			offset2[0] = off2.getAttribute("x").getDoubleValue();
			offset2[1] = off2.getAttribute("y").getDoubleValue();
			offset2[2] = off2.getAttribute("z").getDoubleValue();
			this.zoff1[0] = zoff1.getAttribute("x").getDoubleValue();
			this.zoff1[1] = zoff1.getAttribute("y").getDoubleValue();
			this.zoff1[2] = zoff1.getAttribute("z").getDoubleValue();
			this.zoff2[0] = zoff2.getAttribute("x").getDoubleValue();
			this.zoff2[1] = zoff2.getAttribute("y").getDoubleValue();
			this.zoff2[2] = zoff2.getAttribute("z").getDoubleValue();
			this.offset1_nonp[0] = off_nonp1.getAttribute("x").getDoubleValue();
			this.offset1_nonp[1] = off_nonp1.getAttribute("y").getDoubleValue();
			this.offset1_nonp[2] = off_nonp1.getAttribute("z").getDoubleValue();
			this.offset2_nonp[0] = off_nonp2.getAttribute("x").getDoubleValue();
			this.offset2_nonp[1] = off_nonp2.getAttribute("y").getDoubleValue();
			this.offset2_nonp[2] = off_nonp2.getAttribute("z").getDoubleValue();
		} catch (DataConversionException dce) {
			logger.error("failed conversion");
		}
	}

	AtomicConfiguration getCrystal1() {
		return this.crystal1;
	}

	AtomicConfiguration getCrystal2() {
		return this.crystal2;
	}

	private void init() throws IOException, PmodelException {
		CIFImporter cifimp = new CIFImporter();
		cifimp.createOptionsPanel(false, this, crystal1File.getAbsolutePath());
		AtomCoords[] c1 = cifimp.getAtomCoordsFrom(crystal1File.getAbsolutePath());
		crystal1 = super.getAtomicConfiguration(c1[0]);
		crystal1.setName("crystal1");
		cifimp.createOptionsPanel(false, this, crystal2File.getAbsolutePath());
		AtomCoords[] c2 = cifimp.getAtomCoordsFrom(crystal2File.getAbsolutePath());
		crystal2 = super.getAtomicConfiguration(c2[0]);
		crystal2.setName("crystal2");
		binv1 = false;
		binv2 = false;
		setName("interface");
		genInterface();
	}

	private void genInterface() {
		double[][] latvec1 = crystal1.getLatVec();
		double[][] latvec2 = crystal2.getLatVec();
		double[] avec = new double[3];
		double[] bvec = new double[3];
		double[] cvec = new double[3];
		for (int i = 0; i < 3; i++) {
			avec[i] = latvec1[0][i] + latvec2[0][i];
			bvec[i] = latvec1[1][i] + latvec2[1][i];
			cvec[i] = latvec1[2][i] + latvec2[2][i];
		}
		double[][] lv = new double[3][3];
		if (initialAB == INITIAL_AB.AVERAGE)
			for (int i = 0; i < 3; i++) {
				lv[0][i] = avec[i] * 0.5d;
				lv[1][i] = bvec[i] * 0.5d;
				lv[2][i] = cvec[i];
			}
		else if (initialAB == INITIAL_AB.CRYSTAL1)
			for (int i = 0; i < 3; i++) {
				lv[0][i] = latvec1[0][i];
				lv[1][i] = latvec1[1][i];
				lv[2][i] = cvec[i];
				crystal2.latvec[0] = crystal1.latvec[0];
				crystal2.latvec[1] = crystal1.latvec[1];
				crystal2.updateFract();
			}
		else if (initialAB == INITIAL_AB.CRYSTAL2)
			for (int i = 0; i < 3; i++) {
				lv[0][i] = latvec2[0][i];
				lv[1][i] = latvec2[1][i];
				lv[2][i] = cvec[i];
				crystal1.latvec[0] = crystal2.latvec[0];
				crystal1.latvec[1] = crystal2.latvec[1];
				crystal1.updateFract();
			}
		crystal1.pack();
		crystal2.pack();
		double[] fractRatio1 = new double[3];
		double[] fractRatio2 = new double[3];
		for (int i = 0; i < 3; i++) {
			if (lv[2][i] > 1) {
				fractRatio1[i] = latvec1[2][i] / lv[2][i];
				fractRatio2[i] = latvec2[2][i] / lv[2][i];
			} else {
				fractRatio1[i] = 0d;
				fractRatio2[i] = 0d;
			}
		}
		logger.debug("fractional ratio 1 " + fractRatio1[0] + " " + fractRatio1[1] + " " + fractRatio1[2]);
		logger.debug("fractional ratio 2 " + fractRatio2[0] + " " + fractRatio2[1] + " " + fractRatio2[2]);
		setLatticeVector(lv);
		updateLatticeConstant();
		setNumAt(crystal1.getNumAt() + crystal2.getNumAt());
		if (initialCoord == INITIAL_COORD.FRACTIONAL) {
			for (int i = 0; i < crystal1.getNumAt(); i++) {
				elemname[i] = crystal1.getElementName()[i];
				fractpos[i][0] = crystal1.getFractPos()[i][0];
				fractpos[i][1] = crystal1.getFractPos()[i][1];
				fractpos[i][2] = crystal1.getFractPos()[i][2] * fractRatio1[2];
			}

			for (int i = 0; i < crystal2.getNumAt(); i++) {
				elemname[crystal1.getNumAt() + i] = crystal2.getElementName()[i];
				fractpos[crystal1.getNumAt() + i][0] = crystal2.getFractPos()[i][0] + fractRatio1[0];
				fractpos[crystal1.getNumAt() + i][1] = crystal2.getFractPos()[i][1] + fractRatio1[1];
				fractpos[crystal1.getNumAt() + i][2] = crystal2.getFractPos()[i][2] * fractRatio2[2] + fractRatio1[2];
			}
			updateCart();
		}
		if (initialCoord == INITIAL_COORD.CARTESIAN) {
			for (int i = 0; i < crystal1.getNumAt(); i++) {
				elemname[i] = crystal1.getElementName()[i];
				cartpos[i][0] = crystal1.getCartPos()[i][0];
				cartpos[i][1] = crystal1.getCartPos()[i][1];
				cartpos[i][2] = crystal1.getCartPos()[i][2];
			}

			for (int i = 0; i < crystal2.getNumAt(); i++) {
				elemname[crystal1.getNumAt() + i] = crystal2.getElementName()[i];
				cartpos[crystal1.getNumAt() + i][0] = crystal2.getCartPos()[i][0];
				cartpos[crystal1.getNumAt() + i][1] = crystal2.getCartPos()[i][1];
				cartpos[crystal1.getNumAt() + i][2] = crystal2.getCartPos()[i][2] + latvec1[0][2] + latvec1[1][2]
						+ latvec1[2][2];
			}
			updateFract();
		}
		logger.debug(this);
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		logger.info(evt);
	}

	double[] getOffset(int cid) {
		if (cid == -1)
			return offset0;
		if (cid == 0)
			return offset1;
		if (cid == 1)
			return offset2;
		return null;
	}

	double[] getNonPeriodicOffset(int cid) {
		if (cid == 0)
			return offset1_nonp;
		if (cid == 1)
			return offset2_nonp;
		return null;
	}

	boolean isInverted(int cid) {
		if (cid == 0)
			return binv1;
		if (cid == 1)
			return binv2;
		return false;
	}

	boolean doPeriodicTranslationInterface(double vala, double valb, double valc) {
		if (periodicTranslation == null || periodicTranslation.length != nat) {
			periodicTranslation = new int[nat][3];
			for (int i = 0; i < nat; i++)
				for (int j = 0; j < 3; j++)
					periodicTranslation[i][j] = 0;
		}
		double[] offset = offset0;
		double sa = 0d;
		double sb = 0d;
		double sc = 0d;
		boolean trans = false;
		if (Math.abs(vala - offset[0]) > 1e-5) {
			sa = vala - offset[0];
			trans = true;
		}
		if (Math.abs(valb - offset[1]) > 1e-5) {
			sb = valb - offset[1];
			trans = true;
		}
		if (Math.abs(valc - offset[2]) > 1e-5) {
			sc = valc - offset[2];
			trans = true;
		}
		offset[0] = vala;
		offset[1] = valb;
		offset[2] = valc;
		if (!trans)
			return false;
		double[][] cartpos = getCartPos();
		double[][] norm = getNormalizedLatticeVector();
		for (int i = 0; i < nat; i++)
			for (int j = 0; j < 3; j++)
				cartpos[i][j] += sa * norm[0][j] + sb * norm[1][j] + sc * norm[2][j];
		updateFract();
		pack();
		return true;
	}

	boolean doPeriodicTranslation(int cid, double vala, double valb, double valc, boolean nonperiodic) {
		double[] offset = offset1;
		double[] offset_np = offset1_nonp;
		if (cid == 1) {
			offset = offset2;
			offset_np = offset2_nonp;
		}
		double sa = 0d;
		double sb = 0d;
		double sc = 0d;
		boolean trans = false;
		boolean cchanged = false;
		if (Math.abs(vala - offset[0]) > 1e-5) {
			sa = vala - offset[0];
			trans = true;
		}
		if (Math.abs(valb - offset[1]) > 1e-5) {
			sb = valb - offset[1];
			trans = true;
		}
		if (!nonperiodic && Math.abs(valc - offset[2]) > 1e-5) {
			sc = valc - offset[2];
			trans = true;
			cchanged = true;
		}
		if (nonperiodic && Math.abs(valc - offset_np[2]) > 1e-5) {
			sc = valc - offset_np[2];
			trans = true;
			cchanged = true;
		}
		offset[0] = vala;
		offset[1] = valb;
		if (nonperiodic)
			offset_np[2] = valc;
		else
			offset[2] = valc;
		if (trans) {
			int nat = getCrystal1().getNumAt();
			int noff = 0;
			double[][] lvec = getCrystal1().getLatVec();
			double[] zoff = { zoff1[0] + offset0[0], zoff1[1] + offset0[1], zoff1[2] + offset0[2] };
			double[][] norm = getNormalizedLatticeVector();
			double[][] cartpos = getCartPos();
			if (nonperiodic && cchanged && cid == 0)
				for (int i = 0; i < 3; i++)
					zoff1[i] += sa * norm[0][i] + sb * norm[1][i] + sc * norm[2][i];
			if (cid == 1) {
				nat = getCrystal2().getNumAt();
				noff = getCrystal1().getNumAt();
				lvec = getCrystal2().getLatVec();
				double[][] latvec = getCrystal1().getLatVec();
				for (int i = 0; i < 3; i++)
					zoff[i] = latvec[2][i] + zoff2[i] + offset0[i];
				if (nonperiodic && cchanged)
					for (int i = 0; i < 3; i++)
						zoff2[i] += sa * norm[0][i] + sb * norm[1][i] + sc * norm[2][i];
			}
			for (int i = noff; i < nat + noff; i++) {
				if (cartpos[i][2] < offset0[2])
					cartpos[i][2] += latvec[2][2];
				for (int j = 0; j < 3; j++)
					cartpos[i][j] += sa * norm[0][j] + sb * norm[1][j] + sc * norm[2][j];
			}
			updateFract(0);
			updateFract(1);
			pack(0);
			pack(1);
			double[][] carttmp = new double[nat][3];
			for (int i = noff; i < noff + nat; i++)
				for (int j = 0; j < 3; j++)
					carttmp[i - noff][j] = cartpos[i][j] - zoff[j];
			double[][] fract = AtomicConfiguration.getFract(nat, carttmp, lvec);
			if (!nonperiodic)
				for (int i = 0; i < nat; i++)
					pack(fract[i]);
			double[][] cart = AtomicConfiguration.getCart(nat, fract, lvec);
			for (int i = 0; i < nat; i++) {
				for (int j = 0; j < 3; j++)
					cartpos[i + noff][j] = 0d;
				if (cart[i][2] >= latvec[2][2] - offset0[2] + sc * norm[2][2])
					for (int j = 0; j < 3; j++)
						cartpos[i + noff][2] = -getLatVec()[j][2];
				for (int j = 0; j < 3; j++)
					cartpos[i + noff][j] += cart[i][j] + zoff[j];
			}
			updateFract(2);
			pack(2);
			updateCart();
		}
		return trans;
	}

	boolean invert(int cid, boolean currVal) {
		boolean oldval = binv1;
		if (cid == 1)
			oldval = binv2;
		if (currVal == oldval)
			return false;
		int nat = getCrystal1().getNumAt();
		int noff = 0;
		double[][] lvec = getCrystal1().getLatVec();
		double[] zoff = { zoff1[0], zoff1[1], zoff1[2] + offset0[2] };
		if (cid == 1) {
			nat = getCrystal2().getNumAt();
			noff = getCrystal1().getNumAt();
			lvec = getCrystal2().getLatVec();
			double[][] latvec = getCrystal1().getLatVec();
			for (int i = 0; i < 3; i++)
				zoff[i] = latvec[2][i] + zoff2[i];
			zoff[2] += offset0[2];
			logger.debug("zoff : " + zoff[0] + " " + zoff[1] + " " + zoff[2]);
		}
		double[][] fract = getFractWRT(cid, nat, noff, lvec, zoff);
		for (int i = 0; i < nat; i++) {
			fract[i][2] = 1d - fract[i][2];
			pack(fract[i]);
		}
		double[][] cart = AtomicConfiguration.getCart(nat, fract, lvec);
		for (int i = 0; i < nat; i++) {
			for (int j = 0; j < 3; j++)
				cartpos[i + noff][j] = 0d;
			if (cart[i][2] > latvec[2][2] - zoff[2])
				for (int j = 0; j < 3; j++)
					cartpos[i + noff][2] = -getLatVec()[j][2];
			for (int j = 0; j < 3; j++)
				cartpos[i + noff][j] += cart[i][j] + zoff[j];
		}
		if (cid == 0)
			binv1 = currVal;
		else
			binv2 = currVal;
		return true;
	}

	private double[][] getFractWRT(int cid, int nat, int noff, double[][] lvec, double[] zoff) {
		double[][] cartpos = getCartPos();
		double[][] carttmp = new double[nat][3];
		for (int i = noff; i < noff + nat; i++) {
			carttmp[i - noff][2] = 0d;
			if (cartpos[i][2] < zoff[2])
				carttmp[i - noff][2] = latvec[2][2];
		}
		for (int i = noff; i < noff + nat; i++)
			for (int j = 0; j < 3; j++)
				carttmp[i - noff][j] += cartpos[i][j] - zoff[j];
		double[][] fr = AtomicConfiguration.getFract(nat, carttmp, lvec);
		for (int i = 0; i < nat; i++)
			pack(fr[i]);
		return fr;
	}

	void mergeAtomCoords(AtomCoords coor) {
		AtomicConfiguration tconf = getAtomicConfiguration(coor);
		double[] mindiff1 = { 1e+10, 1e+10, 1e+10 };
		double[] mindiff2 = { 1e+10, 1e+10, 1e+10 };
		int natm = nat;
		if (tconf.nat < nat) {
			logger.error("the number of atoms has changed; merge is deemed to fail...");
			natm = tconf.nat;
		}
		int nat1 = getCrystal1().getNumAt();
		for (int i = 0; i < nat1; i++)
			for (int j = 0; j < 3; j++)
				if (Math.abs(cartpos[i][j] - tconf.cartpos[i][j]) < Math.abs(mindiff1[j]))
					mindiff1[j] = cartpos[i][j] - tconf.cartpos[i][j];
		for (int j = 0; j < 3; j++)
			if (Math.abs(mindiff1[j]) > 1e-10) {
				offset1[j] -= mindiff1[j];
				logger.debug("mindiff1 for crystal1, direction " + j + " : " + mindiff1[j]);
			}

		for (int i = nat1; i < natm; i++)
			for (int j = 0; j < 3; j++)
				if (Math.abs(cartpos[i][j] - tconf.cartpos[i][j]) < Math.abs(mindiff2[j]))
					mindiff2[j] = cartpos[i][j] - tconf.cartpos[i][j];
		for (int j = 0; j < 3; j++)
			if (Math.abs(mindiff2[j]) > 1e-10) {
				offset2[j] -= mindiff2[j];
				logger.debug("mindiff2 for crystal1, direction " + j + " : " + mindiff2[j]);
			}
		super.mergeAtomCoords(coor);
	}

}