// $Id:
// FORESTER -- software libraries and applications
// for evolutionary biology research and applications.
//
// Copyright (C) 2008-2009 Christian M. Zmasek
// Copyright (C) 2008-2009 Burnham Institute for Medical Research
// All rights reserved
//
// 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
//
// Contact: phylosoft @ gmail . com
// WWW: https://sites.google.com/site/cmzmasek/home/software/forester

package org.forester.archaeopteryx;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.math.BigDecimal;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JEditorPane;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.text.Position;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.forester.archaeopteryx.tools.ImageLoader;
import org.forester.io.parsers.phyloxml.PhyloXmlDataFormatException;
import org.forester.phylogeny.PhylogenyNode;
import org.forester.phylogeny.data.Accession;
import org.forester.phylogeny.data.BranchWidth;
import org.forester.phylogeny.data.Confidence;
import org.forester.phylogeny.data.Date;
import org.forester.phylogeny.data.Distribution;
import org.forester.phylogeny.data.Event;
import org.forester.phylogeny.data.Identifier;
import org.forester.phylogeny.data.MultipleUris;
import org.forester.phylogeny.data.PhylogenyData;
import org.forester.phylogeny.data.PhylogenyDataUtil;
import org.forester.phylogeny.data.Point;
import org.forester.phylogeny.data.Reference;
import org.forester.phylogeny.data.Sequence;
import org.forester.phylogeny.data.Taxonomy;
import org.forester.phylogeny.data.Uri;
import org.forester.util.FailedConditionCheckException;
import org.forester.util.ForesterUtil;

class NodeEditPanel extends JPanel {

    private enum PHYLOXML_TAG {
        NODE_NAME,
        NODE_BRANCH_LENGTH,
        NODE_BRANCH_WIDTH,
        TAXONOMY_CODE,
        TAXONOMY_SCIENTIFIC_NAME,
        TAXONOMY_AUTHORITY,
        TAXONOMY_COMMON_NAME,
        TAXONOMY_SYNONYM,
        TAXONOMY_RANK,
        TAXONOMY_URI,
        SEQ_SYMBOL,
        SEQ_NAME,
        SEQ_GENE_NAME,
        SEQ_LOCATION,
        SEQ_TYPE,
        SEQ_MOL_SEQ,
        SEQ_URI,
        DATE_DESCRIPTION,
        DATE_VALUE,
        DATE_MIN,
        DATE_MAX,
        DATE_UNIT,
        TAXONOMY_ID_VALUE,
        TAXONOMY_ID_PROVIDER,
        SEQ_ACC_VALUE,
        SEQ_ACC_SOURCE,
        CONFIDENCE_VALUE,
        CONFIDENCE_TYPE,
        LIT_REFERENCE_DESC,
        LIT_REFERENCE_DOI,
        EVENTS_DUPLICATIONS,
        EVENTS_SPECIATIONS,
        EVENTS_GENE_LOSSES,
        DIST_DESC,
        DIST_GEODETIC,
        DIST_LAT,
        DIST_LONG,
        DIST_ALT,
        DIST_ALT_UNIT
    }

    private class TagNumber {

        final private PHYLOXML_TAG _tag;
        final private int          _number;

        TagNumber( final PHYLOXML_TAG tag, final int number ) {
            _tag = tag;
            _number = number;
        }

        @Override
        public String toString() {
            return getTag() + "_" + getNumber();
        }

        int getNumber() {
            return _number;
        }

        PHYLOXML_TAG getTag() {
            return _tag;
        }
    }
    private static final long                            serialVersionUID = 5120159904388100771L;
    private final JTree                                  _tree;
    private final JEditorPane                            _pane;
    private final PhylogenyNode                          _my_node;
    private final TreePanel                              _tree_panel;
    private final Map<DefaultMutableTreeNode, TagNumber> _map;

    public NodeEditPanel( final PhylogenyNode phylogeny_node, final TreePanel tree_panel ) {
        _map = new HashMap<DefaultMutableTreeNode, TagNumber>();
        _my_node = phylogeny_node;
        _tree_panel = tree_panel;
        String node_name = "";
        if ( !ForesterUtil.isEmpty( phylogeny_node.getName() ) ) {
            node_name = phylogeny_node.getName() + " ";
        }
        final DefaultMutableTreeNode top = new DefaultMutableTreeNode( "Node " + node_name );
        createNodes( top, phylogeny_node );
        _tree = new JTree( top );
        getJTree().setEditable( true );
        getJTree().setFocusable( true );
        getJTree().setToggleClickCount( 1 );
        getJTree().setInvokesStopCellEditing( true );
        final JScrollPane tree_view = new JScrollPane( getJTree() );
        _pane = new JEditorPane();
        _pane.setEditable( true );
        final JScrollPane data_view = new JScrollPane( _pane );
        final JSplitPane split_pane = new JSplitPane( JSplitPane.VERTICAL_SPLIT );
        split_pane.setTopComponent( tree_view );
        // split_pane.setBottomComponent( data_view );
        data_view.setMinimumSize( Constants.NODE_PANEL_SPLIT_MINIMUM_SIZE );
        tree_view.setMinimumSize( Constants.NODE_PANEL_SPLIT_MINIMUM_SIZE );
        // split_pane.setDividerLocation( 400 );
        split_pane.setPreferredSize( Constants.NODE_PANEL_SIZE );
        add( split_pane );
        getJTree().getSelectionModel().setSelectionMode( TreeSelectionModel.SINGLE_TREE_SELECTION );
        getJTree().addKeyListener( new KeyListener() {

            @Override
            public void keyPressed( final KeyEvent e ) {
                keyEvent( e );
            }

            @Override
            public void keyReleased( final KeyEvent e ) {
                keyEvent( e );
            }

            @Override
            public void keyTyped( final KeyEvent e ) {
                keyEvent( e );
            }
        } );
        for( int i = 0; i < getJTree().getRowCount(); i++ ) {
            getJTree().expandRow( i );
        }
        collapsePath( NodePanel.BASIC );
        collapsePath( NodePanel.TAXONOMY );
        collapsePath( NodePanel.SEQUENCE );
        collapsePath( NodePanel.EVENTS );
        collapsePath( NodePanel.DATE );
        collapsePath( NodePanel.DISTRIBUTION );
        collapsePath( NodePanel.LIT_REFERENCE );
        getJTree().addTreeSelectionListener( new TreeSelectionListener() {

            @Override
            public void valueChanged( final TreeSelectionEvent e ) {
                final TreePath new_path = e.getNewLeadSelectionPath();
                final TreePath old_path = e.getOldLeadSelectionPath();
                if ( new_path != null ) {
                    writeBack( ( DefaultMutableTreeNode ) new_path.getLastPathComponent() );
                }
                if ( old_path != null ) {
                    writeBack( ( DefaultMutableTreeNode ) old_path.getLastPathComponent() );
                }
            }
        } );
    }

    private void addBasics( final DefaultMutableTreeNode top, final PhylogenyNode phylogeny_node, final String name ) {
        final DefaultMutableTreeNode category = new DefaultMutableTreeNode( name );
        top.add( category );
        addSubelementEditable( category, NodePanel.NODE_NAME, phylogeny_node.getName(), PHYLOXML_TAG.NODE_NAME );
        String bl = "";
        if ( phylogeny_node.getDistanceToParent() != PhylogenyDataUtil.BRANCH_LENGTH_DEFAULT ) {
            bl = ForesterUtil.FORMATTER_6.format( phylogeny_node.getDistanceToParent() );
        }
        addSubelementEditable( category, NodePanel.NODE_BRANCH_LENGTH, bl, PHYLOXML_TAG.NODE_BRANCH_LENGTH );
        int counter = 0;
        if ( phylogeny_node.getBranchData().isHasConfidences() ) {
            for( int i = phylogeny_node.getBranchData().getConfidences().size() - 1; i >= 0; i-- ) {
                if ( phylogeny_node.getBranchData().getConfidences().get( i ).getValue() == Confidence.CONFIDENCE_DEFAULT_VALUE ) {
                    phylogeny_node.getBranchData().getConfidences().remove( i );
                }
            }
            for( final PhylogenyData conf : phylogeny_node.getBranchData().getConfidences() ) {
                final Confidence my_conf = ( Confidence ) ( conf );
                addSubelementEditable( category,
                                       NodePanel.CONFIDENCE + " [" + counter + "]",
                                       ForesterUtil.FORMATTER_6.format( my_conf.getValue() ),
                                       PHYLOXML_TAG.CONFIDENCE_VALUE,
                                       NodePanel.CONFIDENCE_TYPE,
                                       my_conf.getType(),
                                       PHYLOXML_TAG.CONFIDENCE_TYPE,
                                       counter++ );
            }
        }
        addSubelementEditable( category,
                               NodePanel.CONFIDENCE + " [" + counter + "]",
                               "",
                               PHYLOXML_TAG.CONFIDENCE_VALUE,
                               NodePanel.CONFIDENCE_TYPE,
                               "",
                               PHYLOXML_TAG.CONFIDENCE_TYPE,
                               counter );
        String bw = "1";
        if ( ( phylogeny_node.getBranchData().getBranchWidth() != null )
                && ( phylogeny_node.getBranchData().getBranchWidth().getValue() != BranchWidth.BRANCH_WIDTH_DEFAULT_VALUE ) ) {
            bw = ForesterUtil.FORMATTER_3.format( phylogeny_node.getBranchData().getBranchWidth().getValue() );
        }
        addSubelementEditable( category, NodePanel.NODE_BRANCH_WIDTH, bw, PHYLOXML_TAG.NODE_BRANCH_WIDTH );
    }

    //    private void addAnnotation( final DefaultMutableTreeNode top, final Annotation ann, final String name ) {
    //        DefaultMutableTreeNode category;
    //        category = new DefaultMutableTreeNode( name );
    //        top.add( category );
    //        addSubelementEditable( category, "Reference", ann.getRef() , PHYLOXML_TAG.);
    //        addSubelementEditable( category, "Description", ann.getDesc() , PHYLOXML_TAG.);
    //        addSubelementEditable( category, "Source", ann.getSource(), PHYLOXML_TAG. );
    //        addSubelementEditable( category, "Type", ann.getType(), PHYLOXML_TAG. );
    //        addSubelementEditable( category, "Evidence", ann.getEvidence() , PHYLOXML_TAG.);
    //        if ( ann.getConfidence() != null ) {
    //            addSubelementEditable( category, "Confidence", ann.getConfidence().asText().toString() , PHYLOXML_TAG.);
    //        }
    //        if ( ann.getProperties() != null ) {
    //            addProperties( category, ann.getProperties(), "Properties", PHYLOXML_TAG. );
    //        }
    //    }
    //    private void addAnnotations( final DefaultMutableTreeNode top,
    //                                 final List<PhylogenyData> annotations,
    //                                 final DefaultMutableTreeNode category ) {
    //        if ( ( annotations != null ) && ( annotations.size() > 0 ) ) {
    //            category.add( new DefaultMutableTreeNode( "Annotations" ) );
    //            final DefaultMutableTreeNode last = top.getLastLeaf();
    //            int i = 0;
    //            for( final PhylogenyData ann : annotations ) {
    //                addAnnotation( last, ( Annotation ) ann, "Annotation " + ( i++ ) );
    //            }
    //        }
    //    }
    private void addDate( final DefaultMutableTreeNode top, Date date, final String name ) {
        if ( date == null ) {
            date = new Date();
        }
        DefaultMutableTreeNode category;
        category = new DefaultMutableTreeNode( name );
        top.add( category );
        addSubelementEditable( category, NodePanel.DATE_DESCRIPTION, date.getDesc(), PHYLOXML_TAG.DATE_DESCRIPTION );
        addSubelementEditable( category,
                               NodePanel.DATE_VALUE,
                               String.valueOf( date.getValue() != null ? date.getValue() : "" ),
                               PHYLOXML_TAG.DATE_VALUE );
        addSubelementEditable( category,
                               NodePanel.DATE_MIN,
                               String.valueOf( date.getMin() != null ? date.getMin() : "" ),
                               PHYLOXML_TAG.DATE_MIN );
        addSubelementEditable( category,
                               NodePanel.DATE_MAX,
                               String.valueOf( date.getMax() != null ? date.getMax() : "" ),
                               PHYLOXML_TAG.DATE_MAX );
        addSubelementEditable( category, NodePanel.DATE_UNIT, date.getUnit(), PHYLOXML_TAG.DATE_UNIT );
    }

    private void addDistribution( final DefaultMutableTreeNode top, Distribution dist, final String name ) {
        if ( dist == null ) {
            dist = new Distribution( "" );
        }
        final DefaultMutableTreeNode category = new DefaultMutableTreeNode( name );
        top.add( category );
        Point p0 = null;
        if ( ( dist.getPoints() != null ) && ( dist.getPoints().size() > 0 ) ) {
            p0 = dist.getPoints().get( 0 );
        }
        else {
            p0 = new Point();
        }
        addSubelementEditable( category, NodePanel.DIST_DESCRIPTION, dist.getDesc(), PHYLOXML_TAG.DIST_DESC );
        addSubelementEditable( category,
                               NodePanel.DIST_GEODETIC_DATUM,
                               p0.getGeodeticDatum(),
                               PHYLOXML_TAG.DIST_GEODETIC );
        addSubelementEditable( category,
                               NodePanel.DIST_LATITUDE,
                               String.valueOf( p0.getLatitude() != null ? p0.getLatitude() : "" ),
                               PHYLOXML_TAG.DIST_LAT );
        addSubelementEditable( category,
                               NodePanel.DIST_LONGITUDE,
                               String.valueOf( p0.getLongitude() != null ? p0.getLongitude() : "" ),
                               PHYLOXML_TAG.DIST_LONG );
        addSubelementEditable( category,
                               NodePanel.DIST_ALTITUDE,
                               String.valueOf( p0.getAltitude() != null ? p0.getAltitude() : "" ),
                               PHYLOXML_TAG.DIST_ALT );
        addSubelementEditable( category,
                               NodePanel.DIST_ALT_UNIT,
                               String.valueOf( p0.getAltiudeUnit() != null ? p0.getAltiudeUnit() : "" ),
                               PHYLOXML_TAG.DIST_ALT_UNIT );
    }

    private void addEvents( final DefaultMutableTreeNode top, Event events, final String name ) {
        final DefaultMutableTreeNode category = new DefaultMutableTreeNode( name );
        if ( events == null ) {
            events = new Event();
        }
        top.add( category );
        addSubelementEditable( category,
                               NodePanel.EVENTS_DUPLICATIONS,
                               String.valueOf( events.getNumberOfDuplications() >= 0 ? events.getNumberOfDuplications()
                                       : 0 ),
                               PHYLOXML_TAG.EVENTS_DUPLICATIONS );
        addSubelementEditable( category,
                               NodePanel.EVENTS_SPECIATIONS,
                               String.valueOf( events.getNumberOfSpeciations() >= 0 ? events.getNumberOfSpeciations()
                                       : 0 ),
                               PHYLOXML_TAG.EVENTS_SPECIATIONS );
        addSubelementEditable( category,
                               NodePanel.EVENTS_GENE_LOSSES,
                               String.valueOf( events.getNumberOfGeneLosses() >= 0 ? events.getNumberOfGeneLosses() : 0 ),
                               PHYLOXML_TAG.EVENTS_GENE_LOSSES );
    }

    private void addMapping( final DefaultMutableTreeNode mtn, final TagNumber tag ) {
        if ( getMap().containsKey( mtn ) ) {
            throw new IllegalArgumentException( "key " + mtn + " already present" );
        }
        if ( getMap().containsValue( tag ) ) {
            throw new IllegalArgumentException( "value " + tag + " already present" );
        }
        getMap().put( mtn, tag );
    }

    private void addReference( final DefaultMutableTreeNode top, Reference ref, final String name ) {
        if ( ref == null ) {
            ref = new Reference( "" );
        }
        final DefaultMutableTreeNode category = new DefaultMutableTreeNode( name );
        top.add( category );
        addSubelementEditable( category,
                               NodePanel.LIT_REFERENCE_DESC,
                               ref.getDescription(),
                               PHYLOXML_TAG.LIT_REFERENCE_DESC );
        addSubelementEditable( category, NodePanel.LIT_REFERENCE_DOI, ref.getDoi(), PHYLOXML_TAG.LIT_REFERENCE_DOI );
    }

    private void addSequence( final DefaultMutableTreeNode top, Sequence seq, final String name ) {
        if ( seq == null ) {
            seq = new Sequence();
        }
        final DefaultMutableTreeNode category = new DefaultMutableTreeNode( name );
        top.add( category );
        Accession acc = seq.getAccession();
        if ( acc == null ) {
            acc = new Accession( "", "" );
        }
        addSubelementEditable( category, NodePanel.SEQ_NAME, seq.getName(), PHYLOXML_TAG.SEQ_NAME );
        addSubelementEditable( category, NodePanel.SEQ_SYMBOL, seq.getSymbol(), PHYLOXML_TAG.SEQ_SYMBOL );
        addSubelementEditable( category, NodePanel.SEQ_GENE_NAME, seq.getGeneName(), PHYLOXML_TAG.SEQ_GENE_NAME );
        addSubelementEditable( category,
                               NodePanel.SEQ_ACCESSION,
                               acc.getValue(),
                               PHYLOXML_TAG.SEQ_ACC_VALUE,
                               "Source",
                               acc.getSource(),
                               PHYLOXML_TAG.SEQ_ACC_SOURCE );
        addSubelementEditable( category, NodePanel.SEQ_LOCATION, seq.getLocation(), PHYLOXML_TAG.SEQ_LOCATION );
        addSubelementEditable( category, NodePanel.SEQ_TYPE, seq.getType(), PHYLOXML_TAG.SEQ_TYPE );
        addSubelementEditable( category, NodePanel.SEQ_MOL_SEQ, seq.getMolecularSequence(), PHYLOXML_TAG.SEQ_MOL_SEQ );
        int uri_counter = 0;
        if ( seq.getUris() != null ) {
            for( final Uri uri : seq.getUris() ) {
                if ( uri != null ) {
                    addSubelementEditable( category, NodePanel.SEQ_URI + " [" + uri_counter + "]", uri.getValue()
                            .toString(), PHYLOXML_TAG.SEQ_URI, uri_counter++ );
                }
            }
        }
        addSubelementEditable( category,
                               NodePanel.SEQ_URI + " [" + uri_counter + "]",
                               "",
                               PHYLOXML_TAG.SEQ_URI,
                               uri_counter );
        //  addAnnotations( top, seq.getAnnotations(), category );
    }

    private void addSubelementEditable( final DefaultMutableTreeNode node,
                                        final String name,
                                        final String value,
                                        final PHYLOXML_TAG phyloxml_tag ) {
        addSubelementEditable( node, name, value, phyloxml_tag, 0 );
    }

    private void addSubelementEditable( final DefaultMutableTreeNode node,
                                        final String name,
                                        final String value,
                                        final PHYLOXML_TAG phyloxml_tag,
                                        final int number ) {
        String my_value = value;
        if ( ForesterUtil.isEmpty( my_value ) ) {
            my_value = "";
        }
        final DefaultMutableTreeNode name_node = new DefaultMutableTreeNode( name );
        final DefaultMutableTreeNode value_node = new DefaultMutableTreeNode( my_value );
        name_node.add( value_node );
        node.add( name_node );
        addMapping( name_node, new TagNumber( phyloxml_tag, number ) );
    }

    private void addSubelementEditable( final DefaultMutableTreeNode node,
                                        final String name,
                                        final String value,
                                        final PHYLOXML_TAG phyloxml_value_tag,
                                        final String source_name,
                                        final String source_value,
                                        final PHYLOXML_TAG phyloxml_source_tag ) {
        addSubelementEditable( node, name, value, phyloxml_value_tag, source_name, source_value, phyloxml_source_tag, 0 );
    }

    private void addSubelementEditable( final DefaultMutableTreeNode node,
                                        final String name,
                                        final String value,
                                        final PHYLOXML_TAG phyloxml_value_tag,
                                        final String source_name,
                                        final String source_value,
                                        final PHYLOXML_TAG phyloxml_source_tag,
                                        final int number ) {
        String my_value = value;
        if ( ForesterUtil.isEmpty( my_value ) ) {
            my_value = "";
        }
        String my_source_value = source_value;
        if ( ForesterUtil.isEmpty( my_source_value ) ) {
            my_source_value = "";
        }
        final DefaultMutableTreeNode name_node = new DefaultMutableTreeNode( name );
        final DefaultMutableTreeNode source_name_node = new DefaultMutableTreeNode( source_name );
        final DefaultMutableTreeNode source_value_node = new DefaultMutableTreeNode( my_source_value );
        final DefaultMutableTreeNode value_node = new DefaultMutableTreeNode( my_value );
        name_node.add( source_name_node );
        source_name_node.add( source_value_node );
        name_node.add( value_node );
        node.add( name_node );
        addMapping( name_node, new TagNumber( phyloxml_value_tag, number ) );
        addMapping( source_name_node, new TagNumber( phyloxml_source_tag, number ) );
    }

    private void addTaxonomy( final DefaultMutableTreeNode top, Taxonomy tax, final String name ) {
        if ( tax == null ) {
            tax = new Taxonomy();
        }
        final DefaultMutableTreeNode category = new DefaultMutableTreeNode( name );
        top.add( category );
        Identifier id = tax.getIdentifier();
        if ( id == null ) {
            id = new Identifier();
        }
        addSubelementEditable( category,
                               NodePanel.TAXONOMY_IDENTIFIER,
                               id.getValue(),
                               PHYLOXML_TAG.TAXONOMY_ID_VALUE,
                               "Provider",
                               id.getProvider(),
                               PHYLOXML_TAG.TAXONOMY_ID_PROVIDER );
        addSubelementEditable( category, NodePanel.TAXONOMY_CODE, tax.getTaxonomyCode(), PHYLOXML_TAG.TAXONOMY_CODE );
        addSubelementEditable( category,
                               NodePanel.TAXONOMY_SCIENTIFIC_NAME,
                               tax.getScientificName(),
                               PHYLOXML_TAG.TAXONOMY_SCIENTIFIC_NAME );
        addSubelementEditable( category,
                               NodePanel.TAXONOMY_AUTHORITY,
                               tax.getAuthority(),
                               PHYLOXML_TAG.TAXONOMY_AUTHORITY );
        addSubelementEditable( category,
                               NodePanel.TAXONOMY_COMMON_NAME,
                               tax.getCommonName(),
                               PHYLOXML_TAG.TAXONOMY_COMMON_NAME );
        for( int i = tax.getSynonyms().size() - 1; i >= 0; i-- ) {
            if ( ForesterUtil.isEmpty( tax.getSynonyms().get( i ) ) ) {
                tax.getSynonyms().remove( i );
            }
        }
        int syn_counter = 0;
        for( final String syn : tax.getSynonyms() ) {
            addSubelementEditable( category,
                                   NodePanel.TAXONOMY_SYNONYM + " [" + syn_counter + "]",
                                   syn,
                                   PHYLOXML_TAG.TAXONOMY_SYNONYM,
                                   syn_counter++ );
        }
        addSubelementEditable( category,
                               NodePanel.TAXONOMY_SYNONYM + " [" + syn_counter + "]",
                               "",
                               PHYLOXML_TAG.TAXONOMY_SYNONYM,
                               syn_counter );
        addSubelementEditable( category, NodePanel.TAXONOMY_RANK, tax.getRank(), PHYLOXML_TAG.TAXONOMY_RANK );
        int uri_counter = 0;
        if ( tax.getUris() != null ) {
            for( final Uri uri : tax.getUris() ) {
                if ( uri != null ) {
                    addSubelementEditable( category, NodePanel.TAXONOMY_URI + " [" + uri_counter + "]", uri.getValue()
                            .toString(), PHYLOXML_TAG.TAXONOMY_URI, uri_counter++ );
                }
            }
        }
        addSubelementEditable( category,
                               NodePanel.TAXONOMY_URI + " [" + uri_counter + "]",
                               "",
                               PHYLOXML_TAG.TAXONOMY_URI,
                               uri_counter );
    }

    private void addUri( final DefaultMutableTreeNode mtn, final Uri uri, final int number, final MultipleUris mu ) {
        if ( uri != null ) {
            if ( mu.getUris() == null ) {
                mu.setUris( new ArrayList<Uri>() );
            }
        }
        if ( ( uri != null ) && ( mu.getUris() == null ) ) {
            mu.setUris( new ArrayList<Uri>() );
        }
        if ( ( uri != null ) && ( mu.getUris().size() == number ) ) {
            mu.getUris().add( uri );
        }
        if ( ( mu.getUris() != null ) && ( mu.getUris().size() != number ) ) {
            mu.getUris().set( number, uri );
        }
        final ImageLoader il = new ImageLoader( getTreePanel() );
        new Thread( il ).start();
    }

    private void collapsePath( final String name ) {
        final TreePath tp = getJTree().getNextMatch( name, 0, Position.Bias.Forward );
        if ( tp != null ) {
            getJTree().collapsePath( tp );
        }
    }

    private void createNodes( final DefaultMutableTreeNode top, final PhylogenyNode phylogeny_node ) {
        if ( !phylogeny_node.getNodeData().isHasTaxonomy() ) {
            phylogeny_node.getNodeData().addTaxonomy( new Taxonomy() );
        }
        if ( !phylogeny_node.getNodeData().isHasSequence() ) {
            phylogeny_node.getNodeData().addSequence( new Sequence() );
        }
        if ( !phylogeny_node.getNodeData().isHasDistribution() ) {
            phylogeny_node.getNodeData().addDistribution( new Distribution( "" ) );
        }
        if ( !phylogeny_node.getNodeData().isHasReference() ) {
            phylogeny_node.getNodeData().addReference( new Reference( "" ) );
        }
        addBasics( top, phylogeny_node, NodePanel.BASIC );
        addTaxonomy( top, phylogeny_node.getNodeData().getTaxonomy(), NodePanel.TAXONOMY );
        addSequence( top, phylogeny_node.getNodeData().getSequence(), NodePanel.SEQUENCE );
        if ( !phylogeny_node.isExternal() ) {
            addEvents( top, phylogeny_node.getNodeData().getEvent(), NodePanel.EVENTS );
        }
        addDate( top, phylogeny_node.getNodeData().getDate(), NodePanel.DATE );
        addDistribution( top, phylogeny_node.getNodeData().getDistribution(), NodePanel.DISTRIBUTION );
        addReference( top, phylogeny_node.getNodeData().getReference(), NodePanel.LIT_REFERENCE );
        //  addProperties( top, phylogeny_node.getNodeData().getProperties(), "Properties" );
    }

    private void formatError( final DefaultMutableTreeNode mtn, final PhyloXmlDataFormatException e ) {
        JOptionPane.showMessageDialog( this, e.getMessage(), "Format error", JOptionPane.ERROR_MESSAGE );
        mtn.setUserObject( "" );
        getJTree().repaint();
    }

    private JTree getJTree() {
        return _tree;
    }

    private Map<DefaultMutableTreeNode, TagNumber> getMap() {
        return _map;
    }

    private TagNumber getMapping( final DefaultMutableTreeNode mtn ) {
        return getMap().get( mtn );
    }

    private DefaultMutableTreeNode getSelectedTreeNode() {
        final TreePath selectionPath = getJTree().getSelectionPath();
        if ( selectionPath != null ) {
            final Object[] path = selectionPath.getPath();
            if ( path.length > 0 ) {
                return ( DefaultMutableTreeNode ) path[ path.length - 1 ]; // Last node
            }
        }
        return null;
    }

    private TreePanel getTreePanel() {
        return _tree_panel;
    }

    private void keyEvent( final KeyEvent e ) {
        if ( e.getKeyCode() == KeyEvent.VK_ENTER ) {
            writeBack( getSelectedTreeNode() );
        }
    }

    private List<Point> obtainPoints() {
        ForesterUtil.ensurePresenceOfDistribution( getMyNode() );
        Distribution d = getMyNode().getNodeData().getDistribution();
        if ( d.getPoints() == null ) {
            d = new Distribution( d.getDesc(), new ArrayList<Point>(), d.getPolygons() );
            getMyNode().getNodeData().setDistribution( d );
        }
        final List<Point> ps = d.getPoints();
        if ( ps.isEmpty() ) {
            ps.add( new Point() );
        }
        else if ( ps.get( 0 ) == null ) {
            ps.set( 0, new Point() );
        }
        return ps;
    }

    private BigDecimal parseBigDecimal( final DefaultMutableTreeNode mtn, final String value ) {
        if ( ForesterUtil.isEmpty( value ) ) {
            return new BigDecimal( 0 );
        }
        BigDecimal i = null;
        try {
            i = new BigDecimal( value );
        }
        catch ( final NumberFormatException e ) {
            JOptionPane.showMessageDialog( this, "illegal value: " + value, "Error", JOptionPane.ERROR_MESSAGE );
            mtn.setUserObject( "" );
        }
        return i;
    }

    private int parsePositiveInt( final DefaultMutableTreeNode mtn, final String value ) {
        if ( ForesterUtil.isEmpty( value ) ) {
            return 0;
        }
        int i = -1;
        try {
            i = ForesterUtil.parseInt( value );
        }
        catch ( final ParseException e ) {
            JOptionPane.showMessageDialog( this, "illegal value: " + value, "Error", JOptionPane.ERROR_MESSAGE );
            mtn.setUserObject( "" );
        }
        if ( i < 0 ) {
            JOptionPane.showMessageDialog( this, "illegal value: " + value, "Error", JOptionPane.ERROR_MESSAGE );
            mtn.setUserObject( "" );
        }
        return i;
    }

    private void writeBack( final DefaultMutableTreeNode mtn ) {
        if ( !getMap().containsKey( mtn ) ) {
            final DefaultMutableTreeNode parent = ( DefaultMutableTreeNode ) mtn.getParent();
            if ( getMap().containsKey( parent ) ) {
                writeBack( mtn, getMapping( parent ) );
            }
        }
    }

    private void writeBack( final DefaultMutableTreeNode mtn, final TagNumber tag_number ) {
        if ( tag_number == null ) {
            return;
        }
        String value = mtn.toString();
        if ( value == null ) {
            value = "";
        }
        value = value.replaceAll( "\\s+", " " );
        value = value.trim();
        mtn.setUserObject( value );
        getJTree().repaint();
        final PHYLOXML_TAG tag = tag_number.getTag();
        final int number = tag_number.getNumber();
        switch ( tag ) {
            case NODE_NAME:
                getMyNode().setName( value );
                break;
            case NODE_BRANCH_LENGTH:
                if ( ForesterUtil.isEmpty( value ) ) {
                    getMyNode().setDistanceToParent( PhylogenyDataUtil.BRANCH_LENGTH_DEFAULT );
                }
                else {
                    try {
                        getMyNode().setDistanceToParent( ForesterUtil.parseDouble( value ) );
                    }
                    catch ( final ParseException e ) {
                        JOptionPane.showMessageDialog( this,
                                                       "failed to parse branch length from: " + value,
                                                       "Error",
                                                       JOptionPane.ERROR_MESSAGE );
                        mtn.setUserObject( "" );
                    }
                }
                break;
            case NODE_BRANCH_WIDTH:
                if ( ForesterUtil.isEmpty( value ) || value.equals( "1" ) ) {
                    if ( getMyNode().getBranchData().getBranchWidth() != null ) {
                        getMyNode().getBranchData().setBranchWidth( new BranchWidth() );
                    }
                }
                else {
                    try {
                        final double bw = ForesterUtil.parseDouble( value );
                        if ( bw >= 0 ) {
                            getMyNode().getBranchData().setBranchWidth( new BranchWidth( bw ) );
                        }
                    }
                    catch ( final ParseException e ) {
                        JOptionPane.showMessageDialog( this,
                                                       "failed to parse branch width from: " + value,
                                                       "Error",
                                                       JOptionPane.ERROR_MESSAGE );
                        mtn.setUserObject( "" );
                    }
                }
                break;
            case CONFIDENCE_VALUE:
                double confidence = Confidence.CONFIDENCE_DEFAULT_VALUE;
                if ( !ForesterUtil.isEmpty( value ) ) {
                    try {
                        confidence = ForesterUtil.parseDouble( value );
                    }
                    catch ( final ParseException e ) {
                        JOptionPane.showMessageDialog( this,
                                                       "failed to parse confidence value from: " + value,
                                                       "Error",
                                                       JOptionPane.ERROR_MESSAGE );
                        mtn.setUserObject( "" );
                        break;
                    }
                }
                if ( getMyNode().getBranchData().getConfidences().size() < number ) {
                    throw new FailedConditionCheckException();
                }
                else if ( getMyNode().getBranchData().getConfidences().size() == number ) {
                    if ( confidence >= 0 ) {
                        getMyNode().getBranchData().getConfidences().add( new Confidence( confidence, "unknown" ) );
                    }
                }
                else {
                    final String type = getMyNode().getBranchData().getConfidences().get( number ).getType();
                    final double sd = getMyNode().getBranchData().getConfidences().get( number ).getStandardDeviation();
                    getMyNode().getBranchData().getConfidences().set( number, new Confidence( confidence, type, sd ) );
                }
                break;
            case CONFIDENCE_TYPE:
                if ( getMyNode().getBranchData().getConfidences().size() < number ) {
                    throw new FailedConditionCheckException();
                }
                else if ( getMyNode().getBranchData().getConfidences().size() == number ) {
                    if ( !ForesterUtil.isEmpty( value ) ) {
                        getMyNode().getBranchData().getConfidences().add( new Confidence( 0, value ) );
                    }
                }
                else {
                    final double v = getMyNode().getBranchData().getConfidences().get( number ).getValue();
                    final double sd = getMyNode().getBranchData().getConfidences().get( number ).getStandardDeviation();
                    getMyNode().getBranchData().getConfidences().set( number, new Confidence( v, value, sd ) );
                }
                break;
            case TAXONOMY_CODE:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                try {
                    getMyNode().getNodeData().getTaxonomy().setTaxonomyCode( value );
                }
                catch ( final PhyloXmlDataFormatException e ) {
                    formatError( mtn, e );
                    break;
                }
                break;
            case TAXONOMY_SCIENTIFIC_NAME:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                getMyNode().getNodeData().getTaxonomy().setScientificName( value );
                break;
            case TAXONOMY_COMMON_NAME:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                getMyNode().getNodeData().getTaxonomy().setCommonName( value );
                break;
            case TAXONOMY_RANK:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                try {
                    getMyNode().getNodeData().getTaxonomy().setRank( value.toLowerCase() );
                }
                catch ( final PhyloXmlDataFormatException e ) {
                    formatError( mtn, e );
                    break;
                }
                break;
            case TAXONOMY_AUTHORITY:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                getMyNode().getNodeData().getTaxonomy().setAuthority( value );
                break;
            case TAXONOMY_URI: {
                Uri uri = null;
                if ( !ForesterUtil.isEmpty( value ) ) {
                    try {
                        uri = new Uri( new URL( value ).toURI() );
                    }
                    catch ( final Exception e ) {
                        JOptionPane.showMessageDialog( this,
                                                       "failed to parse URL from: " + value,
                                                       "Error",
                                                       JOptionPane.ERROR_MESSAGE );
                        mtn.setUserObject( "" );
                    }
                }
                if ( uri != null ) {
                    ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                }
                addUri( mtn, uri, number, getMyNode().getNodeData().getTaxonomy() );
                break;
            }
            case TAXONOMY_SYNONYM:
                if ( getMyNode().getNodeData().getTaxonomy().getSynonyms().size() < number ) {
                    throw new FailedConditionCheckException();
                }
                else if ( getMyNode().getNodeData().getTaxonomy().getSynonyms().size() == number ) {
                    if ( !ForesterUtil.isEmpty( value ) ) {
                        ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                        getMyNode().getNodeData().getTaxonomy().getSynonyms().add( value );
                    }
                }
                else {
                    getMyNode().getNodeData().getTaxonomy().getSynonyms().set( number, value );
                }
                break;
            case TAXONOMY_ID_VALUE:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                if ( getMyNode().getNodeData().getTaxonomy().getIdentifier() == null ) {
                    getMyNode().getNodeData().getTaxonomy().setIdentifier( new Identifier( value ) );
                }
                else {
                    final String provider = getMyNode().getNodeData().getTaxonomy().getIdentifier().getProvider();
                    getMyNode().getNodeData().getTaxonomy().setIdentifier( new Identifier( value, provider ) );
                }
                break;
            case TAXONOMY_ID_PROVIDER:
                ForesterUtil.ensurePresenceOfTaxonomy( getMyNode() );
                if ( getMyNode().getNodeData().getTaxonomy().getIdentifier() == null ) {
                    getMyNode().getNodeData().getTaxonomy().setIdentifier( new Identifier( "", value ) );
                }
                else {
                    final String v = getMyNode().getNodeData().getTaxonomy().getIdentifier().getValue();
                    getMyNode().getNodeData().getTaxonomy().setIdentifier( new Identifier( v, value ) );
                }
                break;
            case SEQ_LOCATION:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                getMyNode().getNodeData().getSequence().setLocation( value );
                break;
            case SEQ_MOL_SEQ:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                getMyNode().getNodeData().getSequence().setMolecularSequence( value.replaceAll( "[^a-zA-Z-]", "" ) );
                break;
            case SEQ_NAME:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                getMyNode().getNodeData().getSequence().setName( value );
                break;
            case SEQ_SYMBOL:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                try {
                    getMyNode().getNodeData().getSequence().setSymbol( value );
                }
                catch ( final PhyloXmlDataFormatException e ) {
                    formatError( mtn, e );
                    break;
                }
                break;
            case SEQ_GENE_NAME:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                getMyNode().getNodeData().getSequence().setGeneName( value );
                break;
            case SEQ_TYPE:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                try {
                    getMyNode().getNodeData().getSequence().setType( value.toLowerCase() );
                }
                catch ( final PhyloXmlDataFormatException e ) {
                    formatError( mtn, e );
                    break;
                }
                break;
            case SEQ_ACC_SOURCE:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                if ( getMyNode().getNodeData().getSequence().getAccession() == null ) {
                    getMyNode().getNodeData().getSequence().setAccession( new Accession( "", value ) );
                }
                else {
                    final String v = getMyNode().getNodeData().getSequence().getAccession().getValue();
                    getMyNode().getNodeData().getSequence().setAccession( new Accession( v, value ) );
                }
                break;
            case SEQ_ACC_VALUE:
                ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                if ( getMyNode().getNodeData().getSequence().getAccession() == null ) {
                    getMyNode().getNodeData().getSequence().setAccession( new Accession( value, "" ) );
                }
                else {
                    final String source = getMyNode().getNodeData().getSequence().getAccession().getSource();
                    getMyNode().getNodeData().getSequence().setAccession( new Accession( value, source ) );
                }
                break;
            case SEQ_URI: {
                Uri uri = null;
                if ( !ForesterUtil.isEmpty( value ) ) {
                    try {
                        uri = new Uri( new URL( value ).toURI() );
                    }
                    catch ( final Exception e ) {
                        JOptionPane.showMessageDialog( this,
                                                       "failed to parse URL from: " + value,
                                                       "Error",
                                                       JOptionPane.ERROR_MESSAGE );
                        mtn.setUserObject( "" );
                    }
                }
                if ( uri != null ) {
                    ForesterUtil.ensurePresenceOfSequence( getMyNode() );
                }
                addUri( mtn, uri, number, getMyNode().getNodeData().getSequence() );
                break;
            }
            case LIT_REFERENCE_DESC:
                if ( !getMyNode().getNodeData().isHasReference() ) {
                    getMyNode().getNodeData().setReference( new Reference( "" ) );
                }
                getMyNode().getNodeData().getReference().setValue( value );
                break;
            case LIT_REFERENCE_DOI:
                if ( !getMyNode().getNodeData().isHasReference() ) {
                    getMyNode().getNodeData().setReference( new Reference( "" ) );
                }
                try {
                    getMyNode().getNodeData().getReference().setDoi( value );
                }
                catch ( final PhyloXmlDataFormatException e ) {
                    formatError( mtn, e );
                    break;
                }
                break;
            case EVENTS_DUPLICATIONS:
                if ( !getMyNode().getNodeData().isHasEvent() ) {
                    getMyNode().getNodeData().setEvent( new Event() );
                }
                getMyNode().getNodeData().getEvent().setDuplications( parsePositiveInt( mtn, value ) );
                break;
            case EVENTS_SPECIATIONS:
                if ( !getMyNode().getNodeData().isHasEvent() ) {
                    getMyNode().getNodeData().setEvent( new Event() );
                }
                getMyNode().getNodeData().getEvent().setSpeciations( parsePositiveInt( mtn, value ) );
                break;
            case EVENTS_GENE_LOSSES:
                if ( !getMyNode().getNodeData().isHasEvent() ) {
                    getMyNode().getNodeData().setEvent( new Event() );
                }
                getMyNode().getNodeData().getEvent().setGeneLosses( parsePositiveInt( mtn, value ) );
                break;
            case DATE_DESCRIPTION:
                ForesterUtil.ensurePresenceOfDate( getMyNode() );
                getMyNode().getNodeData().getDate().setDesc( value );
                break;
            case DATE_MAX:
                ForesterUtil.ensurePresenceOfDate( getMyNode() );
                getMyNode().getNodeData().getDate().setMax( parseBigDecimal( mtn, value ) );
                break;
            case DATE_MIN:
                ForesterUtil.ensurePresenceOfDate( getMyNode() );
                getMyNode().getNodeData().getDate().setMin( parseBigDecimal( mtn, value ) );
                break;
            case DATE_UNIT:
                ForesterUtil.ensurePresenceOfDate( getMyNode() );
                getMyNode().getNodeData().getDate().setUnit( value );
                break;
            case DATE_VALUE:
                ForesterUtil.ensurePresenceOfDate( getMyNode() );
                getMyNode().getNodeData().getDate().setValue( parseBigDecimal( mtn, value ) );
                break;
            case DIST_ALT: {
                final BigDecimal new_value = parseBigDecimal( mtn, value );
                if ( new_value != null ) {
                    final List<Point> ps = obtainPoints();
                    final Point p = ps.get( 0 );
                    final Point p_new = new Point( p.getGeodeticDatum(),
                                                   p.getLatitude(),
                                                   p.getLongitude(),
                                                   new_value,
                                                   ForesterUtil.isEmpty( p.getAltiudeUnit() ) ? "?"
                                                           : p.getAltiudeUnit() );
                    ps.set( 0, p_new );
                }
                break;
            }
            case DIST_DESC: {
                ForesterUtil.ensurePresenceOfDistribution( getMyNode() );
                final Distribution d = getMyNode().getNodeData().getDistribution();
                getMyNode().getNodeData().setDistribution( new Distribution( value, d.getPoints(), d.getPolygons() ) );
                break;
            }
            case DIST_GEODETIC: {
                if ( !ForesterUtil.isEmpty( value ) ) {
                    final List<Point> ps = obtainPoints();
                    final Point p = ps.get( 0 );
                    final Point p_new = new Point( value,
                                                   p.getLatitude(),
                                                   p.getLongitude(),
                                                   p.getAltitude(),
                                                   p.getAltiudeUnit() );
                    ps.set( 0, p_new );
                }
                break;
            }
            case DIST_ALT_UNIT: {
                if ( !ForesterUtil.isEmpty( value ) ) {
                    final List<Point> ps = obtainPoints();
                    final Point p = ps.get( 0 );
                    final Point p_new = new Point( p.getGeodeticDatum(),
                                                   p.getLatitude(),
                                                   p.getLongitude(),
                                                   p.getAltitude(),
                                                   value );
                    ps.set( 0, p_new );
                }
                break;
            }
            case DIST_LAT: {
                final BigDecimal new_value = parseBigDecimal( mtn, value );
                if ( new_value != null ) {
                    final List<Point> ps = obtainPoints();
                    final Point p = ps.get( 0 );
                    final Point p_new = new Point( p.getGeodeticDatum(),
                                                   new_value,
                                                   p.getLongitude(),
                                                   p.getAltitude(),
                                                   p.getAltiudeUnit() );
                    ps.set( 0, p_new );
                }
                break;
            }
            case DIST_LONG: {
                final BigDecimal new_value = parseBigDecimal( mtn, value );
                if ( new_value != null ) {
                    final List<Point> ps = obtainPoints();
                    final Point p = ps.get( 0 );
                    final Point p_new = new Point( p.getGeodeticDatum(),
                                                   p.getLatitude(),
                                                   new_value,
                                                   p.getAltitude(),
                                                   p.getAltiudeUnit() );
                    ps.set( 0, p_new );
                }
                break;
            }
            default:
                throw new IllegalArgumentException( "unknown: " + tag );
        }
        getJTree().repaint();
        getTreePanel().setEdited( true );
        getTreePanel().repaint();
    }

    PhylogenyNode getMyNode() {
        return _my_node;
    }

    void writeAll() {
        for( int i = 0; i < getJTree().getRowCount(); i++ ) {
            final TreePath p = getJTree().getPathForRow( i );
            writeBack( ( DefaultMutableTreeNode ) p.getLastPathComponent() );
        }
    }
}
