// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.xml.v0_6;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.codec.binary.Base64;
import org.openstreetmap.osmosis.core.OsmosisConstants;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.container.v0_6.ChangeContainer;
import org.openstreetmap.osmosis.core.task.v0_6.ChangeSink;
import org.openstreetmap.osmosis.xml.v0_6.impl.OsmChangeWriter;


/**
 * An OSM change sink for uploading all data to an OpenStreetMap server.
 *
 * @author Marcus Wolschon Marcus@Wolscon.biz
 */
public class XmlChangeUploader implements ChangeSink {

    /**
     * Our logger for debug and error -output.
     */
    private static final Logger LOG = Logger.getLogger(
            XmlChangeUploader.class.getName());

    /**
     * Default-value for {@link DownloadingDataSet#APIBASEURSETTING}.
     */
    private static final String DEFAULTAPIBASEURL =
             "http://api.openstreetmap.org/api/0.6";

    /**
     * The baseURL defaults to the value of DEFAULTAPIBASEURL.
     */
    private String myBaseURL;

    /**
     * the user-name to use.
     */
    private String myUserName;

    /**
     * the password to use.
     */
    private String myPassword;

    /**
     * Comment to add to the Changeset.
     */
    private String myComment;

    /**
     * Used to generate the XML-content of an osc-file.
     */
    private OsmChangeWriter myChangeWriter;

    /**
     * The ID of the changeset we opened on the server.
     */
    private int myChangesetNumber = -1;

    /**
     * We cache the changeset here to replace the
     * changeset-id later.
     */
    private StringWriter myChangesetBuffer = new StringWriter();

    /**
     * Creates a new instance.
     * The baseURL defaults to the production API.
     * @param aBaseURL may be null
     * @param aUserName the user-name to use
     * @param aPassword the password to use
     * @param aComment the comment to set in the changeset
     */
    public XmlChangeUploader(final String aBaseURL,
                             final String aUserName,
                             final String aPassword,
                             final String aComment) {

        if (aBaseURL == null) {
            this.myBaseURL = DEFAULTAPIBASEURL;
        } else {
            this.myBaseURL = aBaseURL;
        }
        if (aUserName == null) {
            throw new IllegalArgumentException("null username given");
        }
        this.myUserName = aUserName;
        if (aPassword == null) {
            throw new IllegalArgumentException("null password given");
        }
        this.myPassword = aPassword;
        if (aComment == null) {
            this.myComment = "";
        } else {
            this.myComment = aComment;
        }
        this.myPassword = aPassword;

        this.myChangeWriter = new OsmChangeWriter("osmChange", 0);
    }

    /**
     * Open the changeset if it is not yet open.
     * @throws IOException if we cannot contact the server.
     */
    protected final void initialize() throws IOException {
        if (myChangesetNumber == -1) {
            URL url = new URL(this.myBaseURL + "/changeset/create");
            System.err.println("DEBUG: URL= " + url.toString());
            HttpURLConnection httpCon = (HttpURLConnection)
                                      url.openConnection();
            httpCon.setRequestProperty("User-Agent", "Osmosis/" + OsmosisConstants.VERSION);

            // we do not use Authenticator.setDefault()
            // here to stay thread-safe.
            httpCon.setRequestProperty("Authorization", "Basic "
                    + Base64.encodeBase64String(
                            (this.myUserName + ":"
                           + this.myPassword).getBytes("UTF8")));

            httpCon.setDoOutput(true);
            httpCon.setRequestMethod("PUT");
            OutputStreamWriter out = new OutputStreamWriter(
                httpCon.getOutputStream());
            out.write("<osm version=\"0.6\" generator=\"Osmosis "
                    + OsmosisConstants.VERSION + "\">\n"
                    + "\t<changeset>\n");
            out.write("\t\t<tag k=\"created_by\" v=\"Osmosis\"/>\n");
            out.write("\t\t<tag k=\"comment\" v=\""
                           + this.myComment + "\"/>\n");
            out.write("\t</changeset>\n</osm>");
            out.close();

            int responseCode = httpCon.getResponseCode();
            if (responseCode != HttpURLConnection.HTTP_OK) {
                InputStreamReader reader = new InputStreamReader(
                        httpCon.getInputStream());
                LOG.severe(readAll(reader).toString());
                throw new IllegalStateException("Http-Status-code is not"
                        + " 200 OK but " + responseCode
                        + " \"" + httpCon.getResponseMessage()
                        + "\" Error=" + httpCon.getHeaderField("Error"));
            }
            Reader in = new InputStreamReader(httpCon.getInputStream());
            char[] buffer = new char[Byte.MAX_VALUE];
            int len = in.read(buffer);
            int changeset = Integer.parseInt(new String(buffer, 0, len));

            LOG.info("opened changeset with ID: " + changeset);
            this.myChangesetNumber = changeset;

            this.myChangeWriter.setWriter(this.myChangesetBuffer);
            this.myChangeWriter.begin();
        }
    }
    
    
    /**
     * {@inheritDoc}
     */
    public void initialize(Map<String, Object> metaData) {
		// Do nothing.
	}
    

    /**
     * {@inheritDoc}
     */
    public final void process(final ChangeContainer changeContainer) {
        try {
            initialize();

            myChangeWriter.process(changeContainer);
        } catch (IOException e) {
            throw new OsmosisRuntimeException(
                    "Cannot open changeset on server", e);
        }
    }

    /**
     * close the changeset on the server.
     */
    public final void complete() {
        try {
            myChangeWriter.end();
            LOG.fine("complete() called");
            uploadChangeBuffer();
            closeChangeset();
        } catch (Exception e) {
            throw new OsmosisRuntimeException(
                    "cannot upload or close changeset.", e);
        }
    }

    /**
     * Upload the buffered changes to the server,
     * replacing the changeset-number.
     * @throws IOException if we cannot contact the server
     */
    private void uploadChangeBuffer() throws IOException {
        URL url = new URL(this.myBaseURL + "/changeset/"
                        + this.myChangesetNumber + "/upload");
        HttpURLConnection httpCon = (HttpURLConnection) url.openConnection();
        httpCon.setDoOutput(true);

        // we do not use Authenticator.setDefault() here to stay thread-safe.
        httpCon.setRequestProperty("Authorization", "Basic "
                + Base64.encodeBase64String(
                        (this.myUserName + ":"
                       + this.myPassword).getBytes("UTF8")));

        OutputStream out = httpCon.getOutputStream();
        OutputStreamWriter writer = new OutputStreamWriter(out, "UTF8");
        writer.flush();
        String changeSet = this.myChangesetBuffer.getBuffer().toString();
        System.out.println("changeset we got uploading:\n" + changeSet);
        String modified = changeSet.replaceAll("changeset=\"[0-9]*\"",
                                               "changeset=\""
                                             + this.myChangesetNumber + "\"");
        System.out.println("changeset we are uploading:\n" + modified);
        writer.write(modified);
        writer.close();
        int responseCode = httpCon.getResponseCode();
        LOG.fine("response-code to changeset: "
                + responseCode);
        if (responseCode != HttpURLConnection.HTTP_OK) {
//            InputStreamReader reader = new InputStreamReader(
//                    httpCon.getInputStream());
//            LOG.severe("response:\n" + readAll(reader).toString());
            throw new IllegalStateException("Http-Status-code is not"
                    + " 200 OK but " + responseCode
                    + " \"" + httpCon.getResponseMessage()
                    + "\" Error=" + httpCon.getHeaderField("Error"));
        }
    }

    /**
     * Close the changeset on the server,
     * commiting the change.
     * @throws IOException if we cannot contact the server
     */
    private void closeChangeset() throws IOException {
        URL url = new URL(this.myBaseURL + "/changeset/"
                + this.myChangesetNumber + "/close");
        System.err.println("DEBUG: URL= " + url.toString());
        HttpURLConnection httpCon = (HttpURLConnection) url.openConnection();
        httpCon.setDoOutput(true);
        httpCon.setRequestMethod("PUT");

        // we do not use Authenticator.setDefault() here to stay thread-safe.
        httpCon.setRequestProperty("Authorization", "Basic "
                + Base64.encodeBase64String(
                        (this.myUserName + ":"
                       + this.myPassword).getBytes("UTF8")));

        httpCon.setRequestProperty(
                "Content-Type", "application/x-www-form-urlencoded");
        httpCon.connect();
        int responseCode = httpCon.getResponseCode();
        LOG.info("response-code to closing of changeset: "
                + responseCode);
        this.myChangesetNumber = -1;
        if (responseCode != HttpURLConnection.HTTP_OK) {
//            InputStreamReader reader = new InputStreamReader(
//                       httpCon.getInputStream());
//            LOG.severe(readAll(reader).toString());
            throw new IllegalStateException("Http-Status-code is not"
                    + " 200 OK but " + responseCode
                    + " \"" + httpCon.getResponseMessage()
                    + "\" Error=" + httpCon.getHeaderField("Error"));
        }
    }

    /**
     * Read this reader into a String.
     * @param aReader where to read from
     * @return the content
     * @throws IOException if we cannot read
     */
    private StringBuilder readAll(final Reader aReader) throws IOException {
        char[] buffer = new char[Byte.MAX_VALUE];
        int reat = -1;
        StringBuilder sb = new StringBuilder();
        while ((reat = aReader.read(buffer)) >= 0) {
            sb.append(buffer, 0, reat);
        }
        aReader.close();
        return sb;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void close() {
        if (this.myChangesetNumber != -1) {
            try {
                LOG.fine("release() called");
                closeChangeset();
            } catch (Exception e) {
                LOG.log(Level.SEVERE, "Cannot close changeset.", e);
            }
        }
    }
}
