/*
 * HomeAppletRecorder.java 13 Oct 2008
 *
 * Sweet Home 3D, Copyright (c) 2008 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.applet;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;

import com.eteks.sweethome3d.io.ContentRecording;
import com.eteks.sweethome3d.io.DefaultHomeInputStream;
import com.eteks.sweethome3d.io.DefaultHomeOutputStream;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomeRecorder;
import com.eteks.sweethome3d.model.InterruptedRecorderException;
import com.eteks.sweethome3d.model.RecorderException;

/**
 * Recorder that stores homes on a HTTP server.
 * @author Emmanuel Puybaret
 */
public class HomeAppletRecorder implements HomeRecorder {
  private final String           writeHomeURL;
  private final String           readHomeURL;
  private final String           listHomesURL;
  private final String           deleteHomeURL;
  private final ContentRecording contentRecording;
  private long                   availableHomesCacheTime;
  private String []              availableHomesCache;

  /**
   * Creates a recorder that will use the URLs in parameter to write, read and list homes.
   * @see SweetHome3DApplet
   */
  public HomeAppletRecorder(String writeHomeURL, 
                            String readHomeURL,
                            String listHomesURL) {
    this(writeHomeURL, readHomeURL, listHomesURL, true);
  }
  
  /**
   * Creates a recorder that will use the URLs in parameter to write, read and list homes.
   * @see SweetHome3DApplet
   */
  public HomeAppletRecorder(String writeHomeURL, 
                            String readHomeURL,
                            String listHomesURL,
                            boolean includeTemporaryContent) {
    this(writeHomeURL, readHomeURL, listHomesURL, 
        includeTemporaryContent 
            ? ContentRecording.INCLUDE_TEMPORARY_CONTENT
            : ContentRecording.INCLUDE_ALL_CONTENT);
  }
  
  /**
   * Creates a recorder that will use the URLs in parameter to write, read and list homes.
   * @see SweetHome3DApplet
   */
  public HomeAppletRecorder(String writeHomeURL, 
                            String readHomeURL,
                            String listHomesURL,
                            ContentRecording contentRecording) {
    this(writeHomeURL, readHomeURL, listHomesURL, null, contentRecording);
  }
  
  /**
   * Creates a recorder that will use the URLs in parameter to write, read, list and delete homes.
   * @see SweetHome3DApplet
   */
  public HomeAppletRecorder(String writeHomeURL, 
                            String readHomeURL,
                            String listHomesURL,
                            String deleteHomeURL,
                            ContentRecording contentRecording) {
    this.writeHomeURL = writeHomeURL;
    this.readHomeURL = readHomeURL;
    this.listHomesURL = listHomesURL;
    this.deleteHomeURL = deleteHomeURL;
    this.contentRecording = contentRecording;
  }
  
  /**
   * Posts home data to the server URL returned by <code>getHomeSaveURL</code>.
   * @throws RecorderException if a problem occurred while writing home.
   */
  public void writeHome(Home home, String name) throws RecorderException {
    HttpURLConnection connection = null;
    try {
      // Open a stream to server 
      connection = (HttpURLConnection)new URL(this.writeHomeURL).openConnection();
      connection.setRequestMethod("POST");
      String multiPartBoundary = "---------#@&$!d3emohteews!$&@#---------";
      connection.setRequestProperty("Content-Type", "multipart/form-data; charset=UTF-8; boundary=" + multiPartBoundary);
      connection.setDoOutput(true);
      connection.setDoInput(true);
      connection.setUseCaches(false);
      
      // Post home part
      OutputStream out = connection.getOutputStream();
      out.write(("--" + multiPartBoundary + "\r\n").getBytes("UTF-8"));
      out.write(("Content-Disposition: form-data; name=\"home\"; filename=\"" 
          + name.replace('\"', '\'') + "\"\r\n").getBytes("UTF-8"));
      out.write(("Content-Type: application/octet-stream\r\n\r\n").getBytes("UTF-8"));
      out.flush();
      DefaultHomeOutputStream homeOut = createHomeOutputStream(out);
      // Write home with HomeOuputStream
      homeOut.writeHome(home);
      homeOut.flush();
      
      // Post last boundary
      out.write(("\r\n--" + multiPartBoundary + "--\r\n").getBytes("UTF-8"));
      out.close();
      
      // Read response
      InputStream in = connection.getInputStream();
      int read = in.read();
      in.close();
      if (read != '1') {
        throw new RecorderException("Saving home " + name + " failed");
      }
      // Reset availableHomes to force a new request at next getAvailableHomes or exists call
      this.availableHomesCache = null; 
    } catch (InterruptedIOException ex) {
      throw new InterruptedRecorderException("Save " + name + " interrupted");
    } catch (IOException ex) {
      throw new RecorderException("Can't save home " + name, ex);
    } finally {
      if (connection != null) {
        connection.disconnect();
      }
    }
  }

  /**
   * Returns the filter output stream used to write a home in the output stream in parameter.
   */
  private DefaultHomeOutputStream createHomeOutputStream(OutputStream out) throws IOException {
    return new DefaultHomeOutputStream(out, 9, this.contentRecording);
  }

  /**
   * Returns a home instance read from its file <code>name</code>.
   * @throws RecorderException if a problem occurred while reading home, 
   *   or if file <code>name</code> doesn't exist.
   */
  public Home readHome(String name) throws RecorderException {
    URLConnection connection = null;
    DefaultHomeInputStream in = null;
    try {
      // Replace % sequence by %% except %s before formating readHomeURL with home name 
      String readHomeURL = String.format(this.readHomeURL.replaceAll("(%[^s])", "%$1"), 
          URLEncoder.encode(name, "UTF-8"));
      // Open a home input stream to server
      connection = new URL(readHomeURL).openConnection();
      connection.setRequestProperty("Content-Type", "charset=UTF-8");
      connection.setUseCaches(false);
      in = new DefaultHomeInputStream(connection.getInputStream(), this.contentRecording);
      // Read home with HomeInputStream
      Home home = in.readHome();
      return home;
    } catch (InterruptedIOException ex) {
      throw new InterruptedRecorderException("Read " + name + " interrupted");
    } catch (IOException ex) {
      throw new RecorderException("Can't read home from " + name, ex);
    } catch (ClassNotFoundException ex) {
      throw new RecorderException("Missing classes to read home from " + name, ex);
    } finally {
      try {
        if (in != null) {
          in.close();
        }
      } catch (IOException ex) {
        throw new RecorderException("Can't close file " + name, ex);
      }
    }
  }

  /**
   * Returns <code>true</code> if the home <code>name</code> exists.
   */
  public boolean exists(String name) throws RecorderException {
    String [] availableHomes;    
    if (this.availableHomesCache != null 
        && this.availableHomesCacheTime + 100 > System.currentTimeMillis()) {
      // Return available homes list in cache if the cache is less than 100 ms old  
      availableHomes = this.availableHomesCache;
    } else {
      availableHomes = getAvailableHomes();
    }
    for (String home : availableHomes) {
      if (home.equals(name)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns the available homes on server.
   */
  public String [] getAvailableHomes() throws RecorderException {
    URLConnection connection = null;
    InputStream in = null;
    try {
      // Open a stream to server 
      connection = new URL(this.listHomesURL).openConnection();
      connection.setUseCaches(false);
      in = connection.getInputStream();
      String contentEncoding = connection.getContentEncoding();
      if (contentEncoding == null) {
        contentEncoding = "UTF-8";
      }
      Reader reader = new InputStreamReader(in, contentEncoding);
      StringWriter homes = new StringWriter();
      for (int c; (c = reader.read()) != -1; ) {
        homes.write(c);
      }
      String [] availableHomes = homes.toString().split("\n");
      if (availableHomes.length == 1 && availableHomes [0].length() == 0) {
        this.availableHomesCache = new String [0];
      } else {
        this.availableHomesCache = availableHomes;
      }
      this.availableHomesCacheTime = System.currentTimeMillis();
      return this.availableHomesCache;
    } catch (IOException ex) {
      throw new RecorderException("Can't read homes from server", ex);
    } finally {
      try {
        if (in != null) {
          in.close();
        }
      } catch (IOException ex) {
        throw new RecorderException("Can't close connection", ex);
      }
    }
  }
  
  /**
   * Returns a home instance read from its file <code>name</code>.
   * @throws RecorderException if a problem occurred while reading home, 
   *   or if file <code>name</code> doesn't exist.
   */
  public void deleteHome(String name) throws RecorderException {
    if (!isHomeDeletionAvailable()) {
      throw new RecorderException("Deletion isn't available");
    }
    HttpURLConnection connection = null;
    try {
      // Replace % sequence by %% except %s before formating readHomeURL with home name 
      String deletedHomeURL = String.format(this.deleteHomeURL.replaceAll("(%[^s])", "%$1"), 
          URLEncoder.encode(name, "UTF-8"));
      // Send request to server
      connection = (HttpURLConnection)new URL(deletedHomeURL).openConnection();
      connection.setRequestProperty("Content-Type", "charset=UTF-8");
      connection.setUseCaches(false);
      // Read response
      InputStream in = connection.getInputStream();
      int read = in.read();
      in.close();
      if (read != '1') {
        throw new RecorderException("Deleting home " + name + " failed");
      }
      // Reset availableHomes to force a new request at next getAvailableHomes or exists call
      this.availableHomesCache = null; 
    } catch (InterruptedIOException ex) {
      throw new InterruptedRecorderException("Delete " + name + " interrupted");
    } catch (IOException ex) {
      throw new RecorderException("Can't delete home " + name, ex);
    } finally {
      if (connection != null) {
        connection.disconnect();
      }
    }
  }
  
  /**
   * Returns <code>true</code> if this recorder provides a service able to delete homes.
   */
  public boolean isHomeDeletionAvailable() {
    return this.deleteHomeURL != null;
  }

  /**
   * Returns the length of the home data that will be saved by this recorder.
   */
  public long getHomeLength(Home home) throws RecorderException {
    try {
      LengthOutputStream out = new LengthOutputStream();
      DefaultHomeOutputStream homeOut = createHomeOutputStream(out);
      homeOut.writeHome(home);
      homeOut.flush();
      return out.getLength();
    } catch (InterruptedIOException ex) {
      throw new InterruptedRecorderException("Home length computing interrupted");
    } catch (IOException ex) {
      throw new RecorderException("Can't compute home length", ex);
    }
  }
  
  /**
   * An output stream used to evaluate the length of written data.
   */
  private class LengthOutputStream extends OutputStream {
    private long length;
    
    @Override
    public void write(int b) throws IOException {
      this.length++;
    }
    
    public long getLength() {
      return this.length;
    }
  }
}
