public interface ITmx extends Map<Key,ITuv> {
        
    public ITu getTu(Key key);
    
    public String getSourceLanguage();
    
    public String getName();
    
    public int getSize();
    
    public Map<String, String> getMetadata();
    
    public ITmx applyChanges(ResolutionSet resolution);
    
    public void writeTo(File outputFile) throws WriteFailedException;
    
    public Object getUnderlyingRepresentation();
} public interface ITuv {
    
    public String getContent();
    
    public String getLanguage();
    
    public Map<String, String> getMetadata();
    
    public Object getUnderlyingRepresentation();
    
    @Override
    public boolean equals(Object o);
    
    public boolean equivalentTo(ITuv other);
} public class JAXBTmx implements ITmx {

    public static final String DIFF_PROP_TYPE = "x-diff-type";
    public static final String DIFF_PROP_VALUE_ADDED = "added";
    public static final String DIFF_PROP_VALUE_DELETED = "deleted";
    public static final String DIFF_PROP_VALUE_MODIFIED = "modified";
    
    public static final String DIFF_PROP_MODIFIED_TYPE = "x-diff-modified";
    public static final String DIFF_PROP_MODIFIED_VALUE_BEFORE = "before";
    public static final String DIFF_PROP_MODIFIED_VALUE_AFTER = "after";
    
    private static final JAXBContext CONTEXT;
    private static final Unmarshaller UNMARSHALLER;
    private static final Marshaller MARSHALLER;
    /**
     * Special XMLReader with EntityResolver required to resolve system DTD per
     * http://stackoverflow.com/questions/3587196/jaxb-saxparseexception-when-unmarshalling-document-with-relative-path-to-dtd
     */
    private static final XMLReader XMLREADER;
    
    private final PropertyChangeSupport propertySupport;
    
    private Tmx tmx;
    private final String name;
    private File file;
    private Map<Key, ITuv> tuvMap;
    private Map<Key, ITu> tuMap;
    private Map<String, String> tmxMetadata;
    
    private static final String FEATURE_NAMESPACES = "http://xml.org/sax/features/namespaces";
    private static final String FEATURE_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes";
    
    private static final Logger LOGGER = Logger.getLogger(JAXBTmx.class.getName());
    
    static {
        try {
            CONTEXT = JAXBContext.newInstance(Tmx.class);
            UNMARSHALLER = CONTEXT.createUnmarshaller();
            MARSHALLER = CONTEXT.createMarshaller();
            MARSHALLER.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
            // We can't simply use JAXB_FORMATTED_OUTPUT because it will add unwanted
            // whitespace to <seg>s that have complex content; use a custom
            // TmxWriter instead (see `writeTo(File)`).
            //MARSHALLER.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            XMLReader xmlreader = XMLReaderFactory.createXMLReader();
            xmlreader.setFeature(FEATURE_NAMESPACES, true);
            xmlreader.setFeature(FEATURE_NAMESPACE_PREFIXES, true);
            xmlreader.setEntityResolver(new EntityResolver() {
                @Override
                public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                    File systemFile = new File(systemId);
                    return new InputSource(Tmx.class.getResourceAsStream(systemFile.getName()));
                }
            });
            XMLREADER = xmlreader;
        } catch (JAXBException ex) {
            throw new RuntimeException(ex);
        } catch (SAXNotRecognizedException ex) {
            throw new RuntimeException(ex);
        } catch (SAXNotSupportedException ex) {
            throw new RuntimeException(ex);
        } catch (SAXException ex) {
            throw new RuntimeException(ex);
        }
    }
    
    public static JAXBTmx createFromDiff(JAXBTmx tmx1, JAXBTmx tmx2) {
        try {
            // We add properties and move stuff around; clone the TMXs
            // so that this function can be idempotent.
            tmx1 = tmx1.klone();
            tmx2 = tmx2.klone();
        } catch (JAXBException ex) {
            throw new RuntimeException(ex);
        }
        
        DiffAnalysis<Key> set = DiffUtil.mapDiff(tmx1, tmx2);
        
        Tmx tmx = newEmptyTmx(tmx2.tmx);
        Body body = tmx.getBody();
        
        for (Key key : set.added) {
            Tu tu = (Tu) tmx2.tuMap.get(key).getUnderlyingRepresentation();
            body.getTu().add(tu);
            
            Prop prop = new Prop();
            tu.getNoteOrProp().add(prop);
            prop.setType(DIFF_PROP_TYPE);
            prop.setContent(DIFF_PROP_VALUE_ADDED);
        }
        
        for (Key key : set.deleted) {
            Tu tu = (Tu) tmx1.tuMap.get(key).getUnderlyingRepresentation();
            body.getTu().add(tu);
            
            Prop prop = new Prop();
            tu.getNoteOrProp().add(prop);
            prop.setType(DIFF_PROP_TYPE);
            prop.setContent(DIFF_PROP_VALUE_DELETED);
        }
        
        for (Key key : set.modified) {
            JAXBTu tu1 = (JAXBTu) tmx1.tuMap.get(key);
            Tu tu = (Tu) tu1.getUnderlyingRepresentation();
            body.getTu().add(tu);
            
            JAXBTu tu2 = (JAXBTu) tmx2.tuMap.get(key);
            tu.getTuv().add((Tuv) tu2.getTargetTuv().getUnderlyingRepresentation());
            
            Prop prop = new Prop();
            tu.getNoteOrProp().add(prop);
            prop.setType(DIFF_PROP_TYPE);
            prop.setContent(DIFF_PROP_VALUE_MODIFIED);
            
            Prop prop1 = new Prop();
            Tuv tuv1 = (Tuv) tu1.getTargetTuv().getUnderlyingRepresentation();
            tuv1.getNoteOrProp().add(prop1);
            prop1.setType(DIFF_PROP_MODIFIED_TYPE);
            prop1.setContent(DIFF_PROP_MODIFIED_VALUE_BEFORE);
            
            Prop prop2 = new Prop();
            Tuv tuv2 = (Tuv) tu2.getTargetTuv().getUnderlyingRepresentation();
            tuv2.getNoteOrProp().add(prop2);
            prop2.setType(DIFF_PROP_MODIFIED_TYPE);
            prop2.setContent(DIFF_PROP_MODIFIED_VALUE_AFTER);
        }
        
        return new JAXBTmx(tmx, "diff");
    }

    public JAXBTmx(File file) throws Exception {
        propertySupport = new PropertyChangeSupport(this);
        this.name = file.getName();
        this.file = file;
        Source source = new SAXSource(XMLREADER, new InputSource(new FileInputStream(file)));
        this.tmx = (Tmx) UNMARSHALLER.unmarshal(source);
        generateMaps();
    }
    
    private JAXBTmx(Tmx tmx, String name) {
        this.tmx = tmx;
        this.name = name;
        propertySupport = new PropertyChangeSupport(this);
        generateMaps();
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertySupport.addPropertyChangeListener(listener);
    }
    
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propertySupport.removePropertyChangeListener(listener);
    }
    
    @Override
    public String getSourceLanguage() {
        return tmx.getHeader().getSrclang();
    }

    @Override
    public int getSize() {
        return tmx.getBody().getTu().size();
    }
    
    @Override
    public String getName() {
        return name;
    }
    
    private void generateMaps() {
        tuvMap = new HashMap<Key, ITuv>();
        tuMap = new HashMap<Key, ITu>();
        for (Tu rawTu : tmx.getBody().getTu()) {
            JAXBTu tu = new JAXBTu(rawTu, getSourceLanguage());
            Key key = tu.getKey();
            assert(!tuMap.containsKey(key));
            assert(!tuvMap.containsKey(key));
            tuMap.put(key, tu);
            ITuv tuv = tu.getTargetTuv();
            tuvMap.put(key, tuv == null ? JAXBTuv.EMPTY_TUV : tuv);
        }
    }
    
    @Override
    public Map<String, String> getMetadata() {
        if (tmxMetadata == null) {
            tmxMetadata = ReflectionUtil.simplePropsToMap(tmx.getHeader());
            if (file != null) {
                tmxMetadata.put("Path", file.getAbsolutePath());
            }
            tmxMetadata = Collections.unmodifiableMap(tmxMetadata);
        }
        return tmxMetadata;
    }
    
    @Override
    public ITmx applyChanges(ResolutionSet resolution) {
        Tmx originalData;
        try {
            originalData = clone(tmx);
        } catch (JAXBException ex) {
            throw new RuntimeException(ex);
        }
        List<Tu> tus = tmx.getBody().getTu();
        for (Key key : resolution.toDelete) {
            tus.remove((Tu) tuMap.get(key).getUnderlyingRepresentation());
        }
        for (Entry<Key, ITuv> e : resolution.toReplace.entrySet()) {
            Tu tu = (Tu) tuMap.get(e.getKey()).getUnderlyingRepresentation();
            ITuv tuvToRemove = tuvMap.get(e.getKey());
            if (tuvToRemove != null) {
                tu.getTuv().remove((Tuv) tuvToRemove.getUnderlyingRepresentation());
                tu.getTuv().add((Tuv) e.getValue().getUnderlyingRepresentation());
            }
        }
        for (ITu tu : resolution.toAdd.values()) {
            tus.add((Tu) tu.getUnderlyingRepresentation());
        }
        Tmx modifiedData = tmx;
        this.tmx = originalData;
        generateMaps();
        return new JAXBTmx(modifiedData, LocString.get("STM_MERGED_TMX_NAME"));
    }
    
    public JAXBTmx klone() throws JAXBException {
        return new JAXBTmx(clone(tmx), name);
    }
    
    public static Tmx clone(Tmx jaxbObject) throws JAXBException {
        // Inspired by: http://stackoverflow.com/a/10870833/448068
        StringWriter xml = new StringWriter();
        MARSHALLER.marshal(jaxbObject, xml);
        StringReader reader = new StringReader(xml.toString());
        Source source = new SAXSource(XMLREADER, new InputSource(reader));
        return (Tmx) UNMARSHALLER.unmarshal(source);
    }
    
    @Override
    public void writeTo(File output) throws WriteFailedException {
        try {
            File parent = output.getParentFile();
            if (!parent.exists()){
                output.getParentFile().mkdirs();
            }
            OutputStream stream = new FileOutputStream(output);
            XMLStreamWriter writer = XMLOutputFactory.newInstance().createXMLStreamWriter(stream, "UTF-8");
            MARSHALLER.marshal(tmx, new TmxWriter(writer));
        } catch (FileNotFoundException ex) {
            throw new WriteFailedException(ex);
        } catch (JAXBException ex) {
            throw new WriteFailedException(ex);
        } catch (XMLStreamException ex) {
            throw new WriteFailedException(ex);
        }
    }

    @Override
    public Object getUnderlyingRepresentation() {
        return tmx;
    }
    
    private static Tmx newEmptyTmx(Tmx orig) {
        Tmx tmx = new Tmx();
        tmx.setVersion("1.1");
        
        Package pkg = JAXBTmx.class.getPackage();
        Header header = new Header();
        tmx.setHeader(header);
        if (pkg != null) {
            header.setCreationtool(pkg.getImplementationTitle());
            header.setCreationtoolversion(pkg.getImplementationVersion());
        }
        if (orig != null) {
            tmx.setVersion(orig.getVersion());
            header.setOTmf(orig.getHeader().getOTmf());
            header.setSrclang(orig.getHeader().getSrclang());
        }
        
        Body body = new Body();
        tmx.setBody(body);
        
        return tmx;
    }
    
    public static JAXBTmx newEmptyJAXBTmx(JAXBTmx orig) {
        return new JAXBTmx(newEmptyTmx(orig == null ? null : orig.tmx), LocString.get("STM_NEW_TMX_NAME"));
    }

    @Override
    public ITu getTu(Key key) {
        return tuMap.get(key);
    }

    @Override
    public int size() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public boolean isEmpty() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public boolean containsKey(Object key) {
        return tuvMap.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public ITuv get(Object key) {
        return tuvMap.get(key);
    }

    @Override
    public ITuv put(Key key, ITuv value) {
        return tuvMap.put(key, value);
    }

    @Override
    public ITuv remove(Object key) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void putAll(Map<? extends Key, ? extends ITuv> m) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public Set<Key> keySet() {
        return tuvMap.keySet();
    }

    @Override
    public Collection<ITuv> values() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public Set<Entry<Key, ITuv>> entrySet() {
        return tuvMap.entrySet();
    }
} public class Key {
    
    public final String sourceText;
    public Map<String, String> props;
    public final Object foreignKey;
    
    public Key(String sourceText, Object foreignKey) {
        this.sourceText = sourceText;
        this.foreignKey = foreignKey;
    }
    
    public void addProp(String name, String value) {
        if (props == null) {
            props = new HashMap<String, String>();
        }
        props.put(name, value);
    }

    @Override
    public int hashCode() {
        if (foreignKey != null) {
            return foreignKey.hashCode();
        }
        final int prime = 31;
        int result = 1;
        result = prime * result + ((props == null) ? 0 : props.hashCode());
        result = prime * result
                + ((sourceText == null) ? 0 : sourceText.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Key other = (Key) obj;
        if (foreignKey != null && other.foreignKey != null) {
            return foreignKey.equals(other.foreignKey);
        }
        if (props == null) {
            if (other.props != null)
                return false;
        } else if (!props.equals(other.props))
            return false;
        if (sourceText == null) {
            if (other.sourceText != null)
                return false;
        } else if (!sourceText.equals(other.sourceText))
            return false;
        return true;
    }
} public class MergeAnalysis<K,V> {
    public final Set<K> deleted;
    public final Set<K> added;
    public final Map<K, V> modified;
    public final Set<K> conflicts;
    
    public static MergeAnalysis unmodifiableAnalysis(MergeAnalysis analysis) {
        return new MergeAnalysis(Collections.unmodifiableSet(analysis.deleted),
                Collections.unmodifiableSet(analysis.added),
                Collections.unmodifiableMap(analysis.modified),
                Collections.unmodifiableSet(analysis.conflicts));
    }
    
    public MergeAnalysis(Set<K> deleted, Set<K> added, Map<K,V> modified, Set<K> conflicts) {
        this.deleted = deleted;
        this.added = added;
        this.modified = modified;
        this.conflicts = conflicts;
    }
    
    public boolean hasConflicts() {
        return !conflicts.isEmpty();
    }
} public class ResolutionSet {
    public final Set<Key> toDelete;
    public final Map<Key, ITu> toAdd;
    public final Map<Key, ITuv> toReplace;
    
    public static ResolutionSet fromAnalysis(MergeAnalysis<Key,ITuv> analysis, ITmx leftTmx, ITmx rightTmx) {
        Set<Key> toDelete = new HashSet<Key>(analysis.deleted);
        Map<Key,ITu> toAdd = new HashMap<Key,ITu>();
        Map<Key,ITuv> toReplace = new HashMap<Key,ITuv>(analysis.modified);
        
        // Add
        for (Key key : analysis.added) {
            ITu leftTuv = leftTmx.getTu(key);
            if (leftTuv != null) {
                toAdd.put(key, leftTuv);
                continue;
            }
            ITu rightTuv = rightTmx.getTu(key);
            assert(rightTuv != null);
            toAdd.put(key, rightTuv);
        }
        
        return new ResolutionSet(toDelete, toAdd, toReplace);
    }
    
    public static ResolutionSet unmodifiableSet(ResolutionSet set) {
        return new ResolutionSet(Collections.unmodifiableSet(set.toDelete),
                Collections.unmodifiableMap(set.toAdd),
                Collections.unmodifiableMap(set.toReplace));
    }
    
    private ResolutionSet(Set<Key> toDelete, Map<Key, ITu> toAdd, Map<Key, ITuv> toReplace) {
        this.toDelete = toDelete;
        this.toAdd = toAdd;
        this.toReplace = toReplace;
    }
} public abstract class ResolutionStrategy {
    
    public static final ResolutionStrategy BASE = new ResolutionStrategy() {
        @Override
        public ITuv resolveConflict(Key key, ITuv baseTuv, ITuv leftTuv, ITuv rightTuv) {
            return baseTuv;
        }
    };
    
    public static final ResolutionStrategy LEFT = new ResolutionStrategy() {
        @Override
        public ITuv resolveConflict(Key key, ITuv baseTuv, ITuv leftTuv, ITuv rightTuv) {
            return leftTuv;
        }
    };
    
    public static final ResolutionStrategy RIGHT = new ResolutionStrategy() {
        @Override
        public ITuv resolveConflict(Key key, ITuv baseTuv, ITuv leftTuv, ITuv rightTuv) {
            return rightTuv;
        }
    };
        
    public ResolutionStrategy() {}
    
    public ResolutionSet resolve(MergeAnalysis<Key,ITuv> analysis, ITmx baseTmx, ITmx leftTmx, ITmx rightTmx) {
        ResolutionSet resolution = ResolutionSet.fromAnalysis(analysis, leftTmx, rightTmx);
        
        for (Key key : analysis.conflicts) {
            ITuv baseTuv = baseTmx.get(key);
            ITuv leftTuv = leftTmx.get(key);
            ITuv rightTuv = rightTmx.get(key);
            
            ITuv selection = resolveConflict(key, baseTuv, leftTuv, rightTuv);
            
            if (selection == baseTuv) {
                // No change
            } else if (selection == leftTuv) {
                dispatchKey(resolution, key, baseTmx, leftTmx);
            } else if (selection == rightTuv) {
                dispatchKey(resolution, key, baseTmx, rightTmx);
            } else {
                throw new RuntimeException("ResolutionStrategy resolved conflict with unknown ITuv.");
            }
        }
        
        return resolution;
    }

    public abstract ITuv resolveConflict(Key key, ITuv baseTuv, ITuv leftTuv, ITuv rightTuv);
    
    private void dispatchKey(ResolutionSet resolution, Key key, ITmx baseTmx, ITmx thisTmx) {
        if (!thisTmx.containsKey(key)) {
            resolution.toDelete.add(key);
        } else if (baseTmx.containsKey(key)) {
            resolution.toReplace.put(key, thisTmx.get(key));
        } else {
            resolution.toAdd.put(key, thisTmx.getTu(key));
        }
    }
} public class MergeWindow extends javax.swing.JPanel {
    
    public static JFrame newAsFrame(final MergeController controller, boolean isTwoWayMerge) {
        MenuFrame frame = new MenuFrame(LocString.get("STM_MERGE_WINDOW_TITLE"));
        frame.setContentPane(new MergeWindow(frame, controller, isTwoWayMerge));
        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        return frame;
    }
    
    public static JDialog newAsDialog(final MergeController controller, boolean isTwoWayMerge, Window parentWindow) {
        final JDialog dialog = new JDialog(parentWindow, LocString.get("STM_MERGE_WINDOW_TITLE"),
                Dialog.ModalityType.APPLICATION_MODAL);
        dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        dialog.add(new MergeWindow(dialog, controller, isTwoWayMerge));
        return dialog;
    }

    private final Window window;
    
    private final List<AbstractButton> leftRadioButtons = new ArrayList<AbstractButton>();
    private final List<AbstractButton> rightRadioButtons = new ArrayList<AbstractButton>();
    private final List<AbstractButton> centerRadioButtons = new ArrayList<AbstractButton>();
    
    private boolean isTwoWayMerge = false;
    private boolean isDetailMode = false;
    private List<ConflictInfo> conflicts = null;
    private List<MergeCell> detailModeCells = new ArrayList<MergeCell>();
    private int detailModeIndex = -1;
    
    /**
     * Creates new form MergeWindow
     * @param window
     * @param controller
     * @param isTwoWayMerge
     */
    public MergeWindow(final Window window, final MergeController controller, boolean isTwoWayMerge) {
        initComponents();
        
        this.window = window;
        window.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent evt) {
                if (!controller.isConflictsAreResolved()) {
                    if (!controller.isCanCancel()) {
                        // Don't let user exit without resolving.
                        return;
                    }
                    int response = JOptionPane.showConfirmDialog(window,
                        LocString.get("STM_CONFIRM_CLOSE_MESSAGE"),
                        LocString.get("STM_MERGE_WINDOW_TITLE"),
                        JOptionPane.YES_NO_OPTION,
                        JOptionPane.WARNING_MESSAGE);
                    if (response != JOptionPane.YES_OPTION) {
                        return;
                    }
                }
                window.dispose();
            }
        });
        
        this.controller = controller;
        this.isTwoWayMerge = isTwoWayMerge;
        
        // Keep tooltips open. Via:
        // http://www.rgagnon.com/javadetails/java-0528.html
        ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
        
        this.conflicts = controller.getConflicts();
        this.isDetailMode = conflicts.size() <= controller.getListViewThreshold();
        
        initContent();
    }
    
    private void initContent() {
        detailModeCells.clear();
        detailModeIndex = -1;
        
        centerRadioButtons.clear();
        leftRadioButtons.clear();
        rightRadioButtons.clear();
        
        Map<Key, AbstractButton[]> presets = controller.getSelections();
        controller.clearSelections();
        
        conflictInfoPanel.removeAll();
        
        jProgressBar1.setVisible(true);
        jProgressBar1.setValue(0);
        
        if (isDetailMode) {
            conflictInfoPanel.setLayout(new GridLayout(1, 1));
            currentConflict.setVisible(true);
            modeSwtichButton.setText(LocString.get("STM_LIST_VIEW_BUTTON"));
        } else {
            conflictInfoPanel.setLayout(new BoxLayout(conflictInfoPanel, BoxLayout.PAGE_AXIS));
            nextButton.setVisible(false);
            backButton.setVisible(false);
            doneButton.setVisible(true);
            doneButton.setEnabled(controller.isConflictsAreResolved());
            currentConflict.setVisible(false);
            modeSwtichButton.setText(LocString.get("STM_DETAIL_VIEW_BUTTON"));
        }
        
        new InitWorker(presets).execute();
        
        // Beansbinding is broken now for some reason, so set this manually.
        leftFilename.setText(controller.getLeftTmx().getName());
        centerFilename.setText(controller.getBaseTmx().getName());
        rightFilename.setText(controller.getRightTmx().getName());
        leftTextUnits.setText(unitCountConverter.convert(controller.getLeftTmx().getSize()));
        centerTextUnits.setText(unitCountConverter.convert(controller.getBaseTmx().getSize()));
        rightTextUnits.setText(unitCountConverter.convert(controller.getRightTmx().getSize()));
        leftFilename.setToolTipText(MapToTextConverter.mapToHtml(controller.getLeftTmx().getMetadata()));
        centerFilename.setToolTipText(MapToTextConverter.mapToHtml(controller.getBaseTmx().getMetadata()));
        rightFilename.setToolTipText(MapToTextConverter.mapToHtml(controller.getRightTmx().getMetadata()));
        conflictCountLabel.setText(conflictCountConverter.convert(controller.getConflictCount()));
        
        allBaseButton.setVisible(!isTwoWayMerge);
        centerFilename.setVisible(!isTwoWayMerge);
        centerTextUnits.setVisible(!isTwoWayMerge);
        
        GuiUtil.sizeForScreen(this);
    }
    
    private void addMergeCell(MergeCell cell) {
        if (isDetailMode) {
            detailModeCells.add(cell);
            if (detailModeIndex < 0) {
                nextButtonActionPerformed(null);
            }
        } else {
            conflictInfoPanel.add(cell);
        }
    }
    
    private void swapCells(int before, int after) {
        if (before == after) {
            return;
        }
        if (before > -1 && before < conflicts.size()) {
            MergeCell current = detailModeCells.get(before);
            conflictInfoPanel.remove(current);
        }
        if (after > -1 && after < conflicts.size()) {
            MergeCell newCell = detailModeCells.get(after);
            conflictInfoPanel.add(newCell);
            newCell.validate();
            newCell.repaint();
        }
    }
    
    private void updateDetailModeState() {
        currentConflict.setText(LocString.getFormat("STM_DETAIL_VIEW_LOCATION", detailModeIndex + 1, conflicts.size()));
        backButton.setVisible(conflicts.size() > 1);
        backButton.setEnabled(detailModeIndex != 0);
        nextButton.setVisible(conflicts.size() > 1 && detailModeIndex < conflicts.size() - 1);
        nextButton.setEnabled(detailModeCells.get(detailModeIndex).isSelectionPerformed());
        doneButton.setVisible(detailModeIndex == conflicts.size() - 1);
        doneButton.setEnabled(controller.isConflictsAreResolved());
    }
    
    private void activateAllButtons(List<AbstractButton> buttons) {
        for (AbstractButton b : buttons) {
            b.setSelected(true);
        }
        controller.actionPerformed(null);
        doneButton.setEnabled(controller.isConflictsAreResolved());
        if (isDetailMode) {
            updateDetailModeState();
        }
    }
    
    private MergeController getController() {
        return controller;
    }
    
    private ActionListener mergeSelectionListener = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
            nextButton.setEnabled(true);
            doneButton.setEnabled(controller.isConflictsAreResolved());
        }
    };
    
    private class InitWorker extends SwingWorker<List<MergeCell>, MergeCell> {
        
        private final Map<Key, AbstractButton[]> presets;

        public InitWorker(Map<Key, AbstractButton[]> presets) {
            this.presets = presets;
            addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    if ("progress".equals(evt.getPropertyName())) {
                        jProgressBar1.setValue((Integer) evt.getNewValue());
                    }
                }
            });
        }
        
        @Override
        protected List<MergeCell> doInBackground() throws Exception {
            List<MergeCell> result = new ArrayList<MergeCell>();

            int n = 1;
            for (ConflictInfo info : conflicts) {
                MergeCell cell = new MergeCell(n, info, isDetailMode);
                cell.setIsTwoWayMerge(isTwoWayMerge);
                result.add(cell);
                publish(cell);
                setProgress(100 * n / conflicts.size());
                n++;
            }
            return result;
        }
        
        @Override
        protected void process(List<MergeCell> chunks) {
            for (MergeCell cell : chunks) {
                AbstractButton[] buttons = cell.getButtons();
                leftRadioButtons.add(buttons[0]);
                centerRadioButtons.add(buttons[1]);
                rightRadioButtons.add(buttons[2]);
                if (presets.size() > 0) {
                    AbstractButton[] preset = presets.get(cell.getKey());
                    if (preset != null) {
                        for (int i = 0; i < preset.length; i++) {
                            if (preset[i].isSelected()) {
                                buttons[i].setSelected(true);
                                break;
                            }
                        }
                    }
                }
                for (AbstractButton button : buttons) {
                    button.addActionListener(mergeSelectionListener);
                }
                controller.addSelection(cell.getKey(), buttons);
                addMergeCell(cell);
            }
            conflictInfoPanel.revalidate();
        }
        
        @Override
        protected void done() {
            jProgressBar1.setVisible(false);
            doneButton.setEnabled(controller.isConflictsAreResolved());
        }
    };
    
/**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        bindingGroup = new org.jdesktop.beansbinding.BindingGroup();

        controller = getController();
        unitCountConverter = new LocStringConverter("STM_NUMBER_OF_UNITS", "STM_NUMBER_OF_UNITS_SINGULAR");
        saveButtonConverter = new org.madlonkay.supertmxmerge.gui.SaveButtonConverter();
        conflictCountConverter = new LocStringConverter("STM_NUMBER_OF_CONFLICTS", "STM_NUMBER_OF_CONFLICTS_SINGULAR");
        mapToTextConverter = new org.madlonkay.supertmxmerge.gui.MapToTextConverter();
        topPanel = new javax.swing.JPanel();
        jPanel2 = new javax.swing.JPanel();
        leftFilename = new javax.swing.JLabel();
        centerFilename = new javax.swing.JLabel();
        rightFilename = new javax.swing.JLabel();
        leftTextUnits = new javax.swing.JLabel();
        centerTextUnits = new javax.swing.JLabel();
        rightTextUnits = new javax.swing.JLabel();
        jPanel6 = new javax.swing.JPanel();
        conflictCountLabel = new javax.swing.JLabel();
        instructionsLabel = new javax.swing.JLabel();
        jPanel5 = new javax.swing.JPanel();
        allLeftButton = new javax.swing.JButton();
        filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
        allBaseButton = new javax.swing.JButton();
        filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
        allRightButton = new javax.swing.JButton();
        jScrollPane1 = new javax.swing.JScrollPane();
        conflictInfoPanel = new org.madlonkay.supertmxmerge.gui.ReasonablySizedPanel();
        bottomPanel = new javax.swing.JPanel();
        modeSwtichButton = new javax.swing.JButton();
        jProgressBar1 = new javax.swing.JProgressBar();
        bottomButtonsPanel = new javax.swing.JPanel();
        filler4 = new javax.swing.Box.Filler(new java.awt.Dimension(4, 0), new java.awt.Dimension(4, 0), new java.awt.Dimension(4, 0));
        currentConflict = new javax.swing.JLabel();
        filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(4, 0), new java.awt.Dimension(4, 0), new java.awt.Dimension(4, 0));
        backButton = new javax.swing.JButton();
        nextButton = new javax.swing.JButton();
        doneButton = new javax.swing.JButton();

        setLayout(new java.awt.BorderLayout());

        topPanel.setLayout(new javax.swing.BoxLayout(topPanel, javax.swing.BoxLayout.PAGE_AXIS));

        jPanel2.setBorder(javax.swing.BorderFactory.createEmptyBorder(4, 4, 4, 4));
        jPanel2.setLayout(new java.awt.GridLayout(2, 3, 10, 0));

        leftFilename.setFont(leftFilename.getFont().deriveFont(leftFilename.getFont().getStyle() | java.awt.Font.BOLD, leftFilename.getFont().getSize()+2));

        org.jdesktop.beansbinding.Binding binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${leftTmx.name}"), leftFilename, org.jdesktop.beansbinding.BeanProperty.create("text"), "leftFileName");
        bindingGroup.addBinding(binding);
        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${leftTmx.metadata}"), leftFilename, org.jdesktop.beansbinding.BeanProperty.create("toolTipText"), "leftTmxMetadata");
        binding.setSourceNullValue(LocString.get("STM_TMX_DETAILS_UNAVAILABLE")); // NOI18N
        binding.setConverter(mapToTextConverter);
        bindingGroup.addBinding(binding);

        jPanel2.add(leftFilename);

        centerFilename.setFont(centerFilename.getFont().deriveFont(centerFilename.getFont().getStyle() | java.awt.Font.BOLD, centerFilename.getFont().getSize()+2));
        centerFilename.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${baseTmx.name}"), centerFilename, org.jdesktop.beansbinding.BeanProperty.create("text"), "baseFileName");
        bindingGroup.addBinding(binding);
        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${baseTmx.metadata}"), centerFilename, org.jdesktop.beansbinding.BeanProperty.create("toolTipText"), "centerTmxMetadata");
        binding.setSourceNullValue(LocString.get("STM_TMX_DETAILS_UNAVAILABLE")); // NOI18N
        binding.setConverter(mapToTextConverter);
        bindingGroup.addBinding(binding);

        jPanel2.add(centerFilename);

        rightFilename.setFont(rightFilename.getFont().deriveFont(rightFilename.getFont().getStyle() | java.awt.Font.BOLD, rightFilename.getFont().getSize()+2));
        rightFilename.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${rightTmx.name}"), rightFilename, org.jdesktop.beansbinding.BeanProperty.create("text"), "rightFileName");
        bindingGroup.addBinding(binding);
        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${rightTmx.metadata}"), rightFilename, org.jdesktop.beansbinding.BeanProperty.create("toolTipText"), "rightTmxMetadata");
        binding.setSourceNullValue(LocString.get("STM_TMX_DETAILS_UNAVAILABLE")); // NOI18N
        binding.setConverter(mapToTextConverter);
        bindingGroup.addBinding(binding);

        jPanel2.add(rightFilename);

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${leftTmx.size}"), leftTextUnits, org.jdesktop.beansbinding.BeanProperty.create("text"), "leftFileUnitCount");
        binding.setConverter(unitCountConverter);
        bindingGroup.addBinding(binding);

        jPanel2.add(leftTextUnits);

        centerTextUnits.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${baseTmx.size}"), centerTextUnits, org.jdesktop.beansbinding.BeanProperty.create("text"), "baseFileUnitCount");
        binding.setConverter(unitCountConverter);
        bindingGroup.addBinding(binding);

        jPanel2.add(centerTextUnits);

        rightTextUnits.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${rightTmx.size}"), rightTextUnits, org.jdesktop.beansbinding.BeanProperty.create("text"), "rightFileUnitCount");
        binding.setConverter(unitCountConverter);
        bindingGroup.addBinding(binding);

        jPanel2.add(rightTextUnits);

        topPanel.add(jPanel2);

        jPanel6.setBorder(javax.swing.BorderFactory.createEmptyBorder(4, 4, 4, 4));
        jPanel6.setLayout(new java.awt.GridLayout(0, 1, 0, 4));

        conflictCountLabel.setForeground(new java.awt.Color(255, 0, 0));
        conflictCountLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        conflictCountLabel.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${conflictCount}"), conflictCountLabel, org.jdesktop.beansbinding.BeanProperty.create("text"), "conflictCount");
        binding.setConverter(conflictCountConverter);
        bindingGroup.addBinding(binding);

        jPanel6.add(conflictCountLabel);

        instructionsLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
        instructionsLabel.setText(LocString.get("STM_MERGE_WINDOW_DIRECTIONS")); // NOI18N
        jPanel6.add(instructionsLabel);

        topPanel.add(jPanel6);

        jPanel5.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createMatteBorder(0, 0, 1, 0, java.awt.SystemColor.controlShadow), javax.swing.BorderFactory.createEmptyBorder(4, 4, 4, 4)));
        jPanel5.setLayout(new javax.swing.BoxLayout(jPanel5, javax.swing.BoxLayout.LINE_AXIS));

        allLeftButton.setText(LocString.get("STM_USE_ALL_MINE_BUTTON")); // NOI18N
        allLeftButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                useAllLeft(evt);
            }
        });
        jPanel5.add(allLeftButton);
        jPanel5.add(filler1);

        allBaseButton.setText(LocString.get("STM_USE_ALL_BASE_BUTTON")); // NOI18N
        allBaseButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                useAllBase(evt);
            }
        });
        jPanel5.add(allBaseButton);
        jPanel5.add(filler2);

        allRightButton.setText(LocString.get("STM_USE_ALL_THEIRS_BUTTON")); // NOI18N
        allRightButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                useAllRight(evt);
            }
        });
        jPanel5.add(allRightButton);

        topPanel.add(jPanel5);

        add(topPanel, java.awt.BorderLayout.NORTH);

        jScrollPane1.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0));
        jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        conflictInfoPanel.setLayout(null);
        jScrollPane1.setViewportView(conflictInfoPanel);

        add(jScrollPane1, java.awt.BorderLayout.CENTER);

        bottomPanel.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createMatteBorder(1, 0, 0, 0, java.awt.SystemColor.controlShadow), javax.swing.BorderFactory.createEmptyBorder(4, 4, 4, 4)));
        bottomPanel.setLayout(new java.awt.BorderLayout());

        modeSwtichButton.setText(LocString.get("STM_LIST_VIEW_BUTTON")); // NOI18N
        modeSwtichButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                modeSwtichButtonActionPerformed(evt);
            }
        });
        bottomPanel.add(modeSwtichButton, java.awt.BorderLayout.WEST);
        bottomPanel.add(jProgressBar1, java.awt.BorderLayout.CENTER);

        bottomButtonsPanel.setLayout(new javax.swing.BoxLayout(bottomButtonsPanel, javax.swing.BoxLayout.LINE_AXIS));
        bottomButtonsPanel.add(filler4);
        bottomButtonsPanel.add(currentConflict);
        bottomButtonsPanel.add(filler3);

        backButton.setText(LocString.get("STM_BACK_BUTTON")); // NOI18N
        backButton.setEnabled(false);
        backButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                backButtonActionPerformed(evt);
            }
        });
        bottomButtonsPanel.add(backButton);

        nextButton.setText(LocString.get("STM_NEXT_BUTTON")); // NOI18N
        nextButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                nextButtonActionPerformed(evt);
            }
        });
        bottomButtonsPanel.add(nextButton);

        doneButton.setText(LocString.get("STM_DONE_BUTTON")); // NOI18N

        binding = org.jdesktop.beansbinding.Bindings.createAutoBinding(org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.READ, controller, org.jdesktop.beansbinding.ELProperty.create("${conflictsAreResolved}"), doneButton, org.jdesktop.beansbinding.BeanProperty.create("enabled"), "conflictsResolved");
        bindingGroup.addBinding(binding);

        doneButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                doneButtonActionPerformed(evt);
            }
        });
        bottomButtonsPanel.add(doneButton);

        bottomPanel.add(bottomButtonsPanel, java.awt.BorderLayout.EAST);

        add(bottomPanel, java.awt.BorderLayout.SOUTH);

        bindingGroup.bind();
    }// </editor-fold>//GEN-END:initComponents

    private void useAllLeft(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useAllLeft
        activateAllButtons(leftRadioButtons);
    }//GEN-LAST:event_useAllLeft

    private void doneButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_doneButtonActionPerformed
        getToolkit().getSystemEventQueue().postEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING));
    }//GEN-LAST:event_doneButtonActionPerformed

    private void useAllBase(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useAllBase
        activateAllButtons(centerRadioButtons);
    }//GEN-LAST:event_useAllBase

    private void useAllRight(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_useAllRight
        activateAllButtons(rightRadioButtons);
    }//GEN-LAST:event_useAllRight

    private void nextButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nextButtonActionPerformed
        int before = detailModeIndex;
        detailModeIndex = Math.min(detailModeCells.size() - 1, detailModeIndex + 1);
        swapCells(before, detailModeIndex);
        updateDetailModeState();
    }//GEN-LAST:event_nextButtonActionPerformed

    private void backButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_backButtonActionPerformed
        int before = detailModeIndex;
        detailModeIndex = Math.max(0, detailModeIndex - 1);
        swapCells(before, detailModeIndex);
        updateDetailModeState();
    }//GEN-LAST:event_backButtonActionPerformed

    private void modeSwtichButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_modeSwtichButtonActionPerformed
        if (isDetailMode) {
            swapCells(detailModeIndex, -1);
        }
        isDetailMode = !isDetailMode;
        initContent();
        validate();
        repaint();
    }//GEN-LAST:event_modeSwtichButtonActionPerformed

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JButton allBaseButton;
    private javax.swing.JButton allLeftButton;
    private javax.swing.JButton allRightButton;
    private javax.swing.JButton backButton;
    private javax.swing.JPanel bottomButtonsPanel;
    private javax.swing.JPanel bottomPanel;
    private javax.swing.JLabel centerFilename;
    private javax.swing.JLabel centerTextUnits;
    private org.madlonkay.supertmxmerge.gui.LocStringConverter conflictCountConverter;
    private javax.swing.JLabel conflictCountLabel;
    private org.madlonkay.supertmxmerge.gui.ReasonablySizedPanel conflictInfoPanel;
    private org.madlonkay.supertmxmerge.MergeController controller;
    private javax.swing.JLabel currentConflict;
    private javax.swing.JButton doneButton;
    private javax.swing.Box.Filler filler1;
    private javax.swing.Box.Filler filler2;
    private javax.swing.Box.Filler filler3;
    private javax.swing.Box.Filler filler4;
    private javax.swing.JLabel instructionsLabel;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JPanel jPanel5;
    private javax.swing.JPanel jPanel6;
    private javax.swing.JProgressBar jProgressBar1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JLabel leftFilename;
    private javax.swing.JLabel leftTextUnits;
    private org.madlonkay.supertmxmerge.gui.MapToTextConverter mapToTextConverter;
    private javax.swing.JButton modeSwtichButton;
    private javax.swing.JButton nextButton;
    private javax.swing.JLabel rightFilename;
    private javax.swing.JLabel rightTextUnits;
    private org.madlonkay.supertmxmerge.gui.SaveButtonConverter saveButtonConverter;
    private javax.swing.JPanel topPanel;
    private org.madlonkay.supertmxmerge.gui.LocStringConverter unitCountConverter;
    private org.jdesktop.beansbinding.BindingGroup bindingGroup;
    // End of variables declaration//GEN-END:variables
} public class DiffUtil {
    
    public static <K,V> DiffAnalysis mapDiff(Map<K,V> before, Map<K,V> after) {
        // Deleted items
        Set<K> deleted = new HashSet<K>(before.keySet());
        deleted.removeAll(after.keySet());
        
        // Added items
        Set<K> added = new HashSet<K>(after.keySet());
        added.removeAll(before.keySet());
        
        // Modified items
        Set<K> modified = new HashSet<K>();
        for (Map.Entry<K,V> e : before.entrySet()) {
            V newItem = after.get(e.getKey());
            if (newItem == null) {
                continue;
            }
            if (!e.getValue().equals(newItem)) {
                modified.add(e.getKey());
            }
        }
        return new DiffAnalysis<K>(deleted, added, modified);
    }
    
    public static <K,V> MergeAnalysis<K,V> mapMerge(Map<K,V> base, Map<K,V> left, Map<K,V> right) {
         
        Set<K> toDelete = new HashSet<K>();
        Set<K> toAdd = new HashSet<K>();
        Map<K,V> toReplace = new HashMap<K,V>();
        Set<K> conflicts = new HashSet<K>();
        
        DiffAnalysis<K> baseToLeft = mapDiff(base, left);
        DiffAnalysis<K> baseToRight = mapDiff(base, right);
        
        // New in left
        for (K key : baseToLeft.added) {
            V leftItem = left.get(key);
            V rightItem = right.get(key);
            if (rightItem == null || leftItem.equals(rightItem)) {
                toAdd.add(key);
            } else {
                conflicts.add(key);
            }
        }
        // New in right
        for (K key : baseToRight.added) {
            if (conflicts.contains(key)) {
                continue;
            }
            V leftItem = left.get(key);
            V rightItem = right.get(key);
            if (leftItem == null) {
                toAdd.add(key);
            } else if (rightItem.equals(leftItem)) {
                // We don't add in this case because it would duplicate when we added
                // in the similar case above.
            } else {
                conflicts.add(key);
            }
        }
        // Deleted from left
        for (K key : baseToLeft.deleted) {
            if (conflicts.contains(key)) {
                continue;
            }
            V rightItem = right.get(key);
            V baseItem = base.get(key);
            if (rightItem == null || baseItem.equals(rightItem)) {
                toDelete.add(key);
            } else {
                conflicts.add(key);
            }
        }
        // Deleted from right
        for (K key : baseToRight.deleted) {
            if (conflicts.contains(key)) {
                continue;
            }
            V leftItem = left.get(key);
            V baseItem = base.get(key);
            if (leftItem == null) {
                // We don't delete in this case because it would duplicate when we deleted
                // in the similar case above.
            } else if (baseItem.equals(leftItem)) {
                toDelete.add(key);
            } else {
                conflicts.add(key);
            }
        }
        // Modified on left
        for (K key : baseToLeft.modified) {
            if (conflicts.contains(key)) {
                continue;
            }
            V baseItem = base.get(key);
            V leftItem = left.get(key);
            V rightItem = right.get(key);
            if (leftItem.equals(rightItem) || baseItem.equals(rightItem)) {
                toReplace.put(key, leftItem);
            } else {
                conflicts.add(key);
            }
        }
        // Modified on right
        for (K key : baseToRight.modified) {
            if (conflicts.contains(key)) {
                continue;
            }
            V leftItem = left.get(key);
            V rightItem = right.get(key);
            V baseItem = base.get(key);
            if (leftItem.equals(rightItem) || baseItem.equals(leftItem)) {
                toReplace.put(key, rightItem);
            } else {
                conflicts.add(key);
            }
        }
        
        return new MergeAnalysis(toDelete, toAdd, toReplace, conflicts);
    }
} public class GuiUtil {
    
    private static final Logger LOGGER = Logger.getLogger(GuiUtil.class.getName());

    public static void displayWindow(Window window) {
        window.pack();
        if (window.getParent() != null) {
            window.setLocationRelativeTo(window.getParent());
        }
        window.setVisible(true);
    }
    
    public static void displayWindowCentered(Window window) {
        window.pack();
        window.setLocationRelativeTo(null);
        window.setVisible(true);
    }
    
    public static void closeWindow(Window window) {
        window.setVisible(false);
        window.dispose();
    }
    
    public static void sizeForScreen(Component component) {
        Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
        int maxHeight =  (int) (screen.height * 0.9);
        int maxWidth = Math.min(maxHeight, (int) (screen.width * 0.9));
        component.setMaximumSize(new Dimension(maxWidth, maxHeight));
        component.setPreferredSize(new Dimension(Math.min(800, maxWidth),
                Math.min(800, maxHeight)));
    }
    
    public static void blockOnWindow(final Window window) {
        final Object lock = new Object();
                
        window.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosed(WindowEvent e) {
                synchronized (lock) {
                    lock.notify();
                }
            }
        });
        
        synchronized (lock) {
            while (window.isVisible()) {
                try {
                    lock.wait();
                } catch (InterruptedException ex) {
                    LOGGER.log(Level.SEVERE, null, ex);
                }
            }
        }
    }
    
    public static void safelyRunBlockingRoutine(Runnable runnable) {
        if (SwingUtilities.isEventDispatchThread()) {
            new Thread(runnable).start();
        } else {
            runnable.run();
        }
    }
    
    public static boolean isOSX() {
        return System.getProperty("os.name").contains("OS X");
    }
    
    public static void forwardMouseWheelEvent(JScrollPane target, MouseWheelEvent evt) {
        Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(
                new MouseWheelEvent(target, evt.getID(), evt.getWhen(),
                        evt.getModifiers(), evt.getX(), evt.getY(),
                        evt.getClickCount(), evt.isPopupTrigger(),
                        evt.getScrollType(), evt.getScrollAmount(), evt.getWheelRotation()));
    }
    
    public static void showError(String message) {
        JOptionPane.showMessageDialog(null,
                message,
                LocString.get("STM_ERROR_DIALOG_TITLE"),
                JOptionPane.ERROR_MESSAGE);
    }
} public class LocString {
    
    private static final ResourceBundle bundle = ResourceBundle.getBundle("org/madlonkay/supertmxmerge/Strings");
    
    private static final List<ResourceBundle> moreBundles = new ArrayList<ResourceBundle>();
    
    private static final Logger LOGGER = Logger.getLogger(LocString.class.getName());
    
    public static String get(String id) {
        for (int i = moreBundles.size() - 1; i >= 0; i--) {
            try {
                return moreBundles.get(i).getString(id);
            } catch (MissingResourceException ex) {
                LOGGER.log(Level.FINE, "Resource " + id + " not found in supplied resource bundle", ex);
            }
        }
        return bundle.getString(id);
    }
    
    public static String getFormat(String id, Object... var) {
        return MessageFormat.format(get(id), var);
    }
    
    public static void addBundle(ResourceBundle bundle) {
        if (bundle == null) {
            return;
        }
        if (!moreBundles.contains(bundle)) {
            moreBundles.add(bundle);
        }
    }
}
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Logger;
import javax.swing.AbstractButton;
import javax.swing.JOptionPane;
import org.madlonkay.supertmxmerge.data.ITmx;
import org.madlonkay.supertmxmerge.data.ITuv;
import org.madlonkay.supertmxmerge.data.JAXB.JAXBTmx;
import org.madlonkay.supertmxmerge.data.Key;
import org.madlonkay.supertmxmerge.data.MergeAnalysis;
import org.madlonkay.supertmxmerge.data.ResolutionSet;
import org.madlonkay.supertmxmerge.data.ResolutionStrategy;
import org.madlonkay.supertmxmerge.gui.MergeWindow;
import org.madlonkay.supertmxmerge.util.DiffUtil;
import org.madlonkay.supertmxmerge.util.GuiUtil;
import org.madlonkay.supertmxmerge.util.LocString;
/*
 * Copyright (C) 2013 Aaron Madlon-Kay <aaron@madlon-kay.com>.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package org.madlonkay.supertmxmerge;


/**
 *
 * @author Aaron Madlon-Kay <aaron@madlon-kay.com>
 */
public class MergeController implements Serializable, ActionListener {
    
    public static final Logger LOGGER = Logger.getLogger(MergeController.class.getName());
    
    public static final String PROP_BASETMX = "baseTmx";
    public static final String PROP_LEFTTMX = "leftTmx";
    public static final String PROP_RIGHTTMX = "rightTmx";
    public static final String PROP_CONFLICTCOUNT = "conflictCount";
    
    public static final String PROP_CONFLICTSARERESOLVED = "conflictsAreResolved";

    private final PropertyChangeSupport propertySupport;
    
    private ITmx baseTmx;
    private ITmx leftTmx;
    private ITmx rightTmx;
        
    private final Map<Key, AbstractButton[]> selections = new HashMap<Key, AbstractButton[]>();
    
    private MergeAnalysis<Key,ITuv> analysis;