// License: GPL. Copyright 2007 by Immanuel Scholz and others
package org.openstreetmap.josm.io;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;

import org.openstreetmap.josm.data.coor.CoordinateFormat;
import org.openstreetmap.josm.data.osm.Changeset;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.DataSource;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Tagged;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.tools.DateUtils;

/**
 * Save the dataset into a stream as osm intern xml format. This is not using any
 * xml library for storing.
 * @author imi
 */
public class OsmWriter extends XmlWriter implements Visitor {

    public final String DEFAULT_API_VERSION = "0.6";

    private boolean osmConform;
    private boolean withBody = true;
    private String version;
    private Changeset changeset;

    public OsmWriter(PrintWriter out, boolean osmConform, String version) {
        super(out);
        this.osmConform = osmConform;
        this.version = (version == null ? DEFAULT_API_VERSION : version);
    }

    public void setWithBody(boolean wb) {
        this.withBody = wb;
    }
    public void setChangeset(Changeset cs) {
        this.changeset = cs;
    }
    public void setVersion(String v) {
        this.version = v;
    }

    public void header() {
        out.println("<?xml version='1.0' encoding='UTF-8'?>");
        out.print("<osm version='");
        out.print(version);
        out.println("' generator='JOSM'>");
    }
    public void footer() {
        out.println("</osm>");
    }

    private static final Comparator<OsmPrimitive> byIdComparator = new Comparator<OsmPrimitive>() {
        public int compare(OsmPrimitive o1, OsmPrimitive o2) {
            return (o1.getUniqueId()<o2.getUniqueId() ? -1 : (o1.getUniqueId()==o2.getUniqueId() ? 0 : 1));
        }
    };

    private Collection<OsmPrimitive> sortById(Collection<? extends OsmPrimitive> primitives) {
        List<OsmPrimitive> result = new ArrayList<OsmPrimitive>(primitives.size());
        result.addAll(primitives);
        Collections.sort(result, byIdComparator);
        return result;
    }

    public void writeContent(DataSet ds) {
        for (OsmPrimitive n : sortById(ds.getNodes())) {
            if (shouldWrite(n)) {
                visit((Node)n);
            }
        }
        for (OsmPrimitive w : sortById(ds.getWays())) {
            if (shouldWrite(w)) {
                visit((Way)w);
            }
        }
        for (OsmPrimitive e: sortById(ds.getRelations())) {
            if (shouldWrite(e)) {
                visit((Relation)e);
            }
        }
    }

    private boolean shouldWrite(OsmPrimitive osm) {
        return !osm.isNew() || !osm.isDeleted();
    }

    public void writeDataSources(DataSet ds) {
        for (DataSource s : ds.dataSources) {
            out.println("  <bounds minlat='"
                    + s.bounds.getMin().lat()+"' minlon='"
                    + s.bounds.getMin().lon()+"' maxlat='"
                    + s.bounds.getMax().lat()+"' maxlon='"
                    + s.bounds.getMax().lon()
                    +"' origin='"+XmlWriter.encode(s.origin)+"' />");
        }
    }

    public void visit(Node n) {
        if (n.isIncomplete()) return;
        addCommon(n, "node");
        out.print(" lat='"+n.getCoor().lat()+"' lon='"+n.getCoor().lon()+"'");
        if (!withBody) {
            out.println("/>");
        } else {
            addTags(n, "node", true);
        }
    }

    public void visit(Way w) {
        if (w.isIncomplete()) return;
        addCommon(w, "way");
        if (!withBody) {
            out.println("/>");
        } else {
            out.println(">");
            for (Node n : w.getNodes()) {
                out.println("    <nd ref='"+n.getUniqueId()+"' />");
            }
            addTags(w, "way", false);
        }
    }

    public void visit(Relation e) {
        if (e.isIncomplete()) return;
        addCommon(e, "relation");
        if (!withBody) {
            out.println("/>");
        } else {
            out.println(">");
            for (RelationMember em : e.getMembers()) {
                out.print("    <member type='");
                out.print(OsmPrimitiveType.from(em.getMember()).getAPIName());
                out.println("' ref='"+em.getMember().getUniqueId()+"' role='" +
                        XmlWriter.encode(em.getRole()) + "' />");
            }
            addTags(e, "relation", false);
        }
    }

    public void visit(Changeset cs) {
        out.print("  <changeset ");
        out.print(" id='"+cs.getId()+"'");
        if (cs.getUser() != null) {
            out.print(" user='"+cs.getUser().getName() +"'");
            out.print(" uid='"+cs.getUser().getId() +"'");
        }
        if (cs.getCreatedAt() != null) {
            out.print(" created_at='"+DateUtils.fromDate(cs.getCreatedAt()) +"'");
        }
        if (cs.getClosedAt() != null) {
            out.print(" closed_at='"+DateUtils.fromDate(cs.getClosedAt()) +"'");
        }
        out.print(" open='"+ (cs.isOpen() ? "true" : "false") +"'");
        if (cs.getMin() != null) {
            out.print(" min_lon='"+ cs.getMin().lonToString(CoordinateFormat.DECIMAL_DEGREES) +"'");
            out.print(" min_lat='"+ cs.getMin().latToString(CoordinateFormat.DECIMAL_DEGREES) +"'");
        }
        if (cs.getMax() != null) {
            out.print(" max_lon='"+ cs.getMin().lonToString(CoordinateFormat.DECIMAL_DEGREES) +"'");
            out.print(" max_lat='"+ cs.getMin().latToString(CoordinateFormat.DECIMAL_DEGREES) +"'");
        }
        out.println(">");
        addTags(cs, "changeset", false); // also writes closing </changeset>
    }

    private static final Comparator<Entry<String, String>> byKeyComparator = new Comparator<Entry<String,String>>() {
        public int compare(Entry<String, String> o1, Entry<String, String> o2) {
            return o1.getKey().compareTo(o2.getKey());
        }
    };

    private void addTags(Tagged osm, String tagname, boolean tagOpen) {
        if (osm.hasKeys()) {
            if (tagOpen) {
                out.println(">");
            }
            List<Entry<String, String>> entries = new ArrayList<Entry<String,String>>(osm.getKeys().entrySet());
            Collections.sort(entries, byKeyComparator);
            for (Entry<String, String> e : entries) {
                if ((osm instanceof Changeset) || !("created_by".equals(e.getKey()))) {
                    out.println("    <tag k='"+ XmlWriter.encode(e.getKey()) +
                            "' v='"+XmlWriter.encode(e.getValue())+ "' />");
                }
            }
            out.println("  </" + tagname + ">");
        } else if (tagOpen) {
            out.println(" />");
        } else {
            out.println("  </" + tagname + ">");
        }
    }

    /**
     * Add the common part as the form of the tag as well as the XML attributes
     * id, action, user, and visible.
     */
    private void addCommon(OsmPrimitive osm, String tagname) {
        out.print("  <"+tagname);
        if (osm.getUniqueId() != 0) {
            out.print(" id='"+ osm.getUniqueId()+"'");
        } else
            throw new IllegalStateException(tr("Unexpected id 0 for osm primitive found"));
        if (!osmConform) {
            String action = null;
            if (osm.isDeleted()) {
                action = "delete";
            } else if (osm.isModified()) {
                action = "modify";
            }
            if (action != null) {
                out.print(" action='"+action+"'");
            }
        }
        if (!osm.isTimestampEmpty()) {
            out.print(" timestamp='"+DateUtils.fromDate(osm.getTimestamp())+"'");
        }
        // user and visible added with 0.4 API
        if (osm.getUser() != null) {
            if(osm.getUser().isLocalUser()) {
                out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+"'");
            } else if (osm.getUser().isOsmUser()) {
                // uid added with 0.6
                out.print(" uid='"+ osm.getUser().getId()+"'");
                out.print(" user='"+XmlWriter.encode(osm.getUser().getName())+"'");
            }
        }
        out.print(" visible='"+osm.isVisible()+"'");
        if (osm.getVersion() != 0) {
            out.print(" version='"+osm.getVersion()+"'");
        }
        if (this.changeset != null && this.changeset.getId() != 0) {
            out.print(" changeset='"+this.changeset.getId()+"'" );
        } else if (osm.getChangesetId() > 0 && !osm.isNew()) {
            out.print(" changeset='"+osm.getChangesetId()+"'" );
        }
    }

    public void close() {
        out.close();
    }

    public void flush() {
        out.flush();
    }
}
