/**
 * This offers TVCards to the network.
 * A client can open the URL for a card (/device/record/<devid>), and ask it to tune and
 * either send the data back as network stream or save it in the local store.
 * 
 * Parameter:
 * <devicerecord>
 *   <channel id="24"/>
 *   <schedule endtime="2007-11-01 12:30:00" />
 *   <target store="foo-disk1" filename="20071101-2015-7.mpg" /> -- filename is relative to storage root
 *   or <target network="true" /> -- stream will be offered to network. URL to fetch from will be returned
 * </devicerecord>
 * 
 * Response:
 * <devicerecordresult>
 *   <source url="http://thishost/stream/<no>" />
 * </devicerecordresult>
 */

package org.bucksch.zeipis.recorder;
import org.bucksch.zeipis.all.*;
import org.bucksch.zeipis.core.*;
import org.bucksch.zeipis.store.*;
import org.bucksch.httpserver.*;
import java.io.*;
import java.util.Map;

public class TVCardProvider implements HTTPHandler
{
  public TVCardProvider()
  {
    CoreServer.me.dispatchingServer().registerHandler(this, "device/start/");
    CoreServer.me.dispatchingServer().registerHandler(this, "device/stop/");
    myOngoingRecordings = Util.newMap();
  }
  public void handle(HTTPRequest request)
    throws HTTPException, IOException
  {
    // Permission
    // Requires working DNS
    //if (!Util.sameHost(request.client().getHostName(), CoreServer.me.coreServerName()))
    //  throw new HTTPException(403, "You are not the core server", request.client().getHostName());

    // Device (from URL)
    int deviceid;
    try {
      deviceid = Integer.valueOf(request.getURLRest());
    } catch (Exception e) {
      throw new HTTPException(400, "URL device/start/ must be followed by a number", request.getURLRest());
    }
    if ( ! CoreData.me.myCards.containsKey(deviceid))
      throw new HTTPException(404, "No such device", deviceid + "");
    Device device = CoreData.me.myCards.get(deviceid); 
    if (!device.isLocal())
      throw new HTTPException(404, "Not a local device", deviceid + "");
    if (!(device instanceof TVCard))
      throw new HTTPException(400, "This is a not a TV card", deviceid + "");
    TVCard card = (TVCard) device;

    if (request.getURLPrefix().equals("device/start/"))
      startRecording(request, card);
    else if (request.getURLPrefix().equals("device/stop/"))
      stopRecording(card);
    else
      throw new HTTPException(404, "Unexpected URL", request.getURLPrefix());
  }

  protected void startRecording(final HTTPRequest request, final TVCard card)
      throws HTTPException, IOException
  {
    // Get the command parameters
    Util.debug("trying to read request params");
    SimpleRFCParams params = new SimpleRFCParams();
    params.parseFromHeaders(request);
    params.parseFromURL(request.getURL()); // allow both (HTTP headers and URL query params)
    int channelid = Integer.valueOf(params.param("channel-id"));
    boolean sendToCaller = "true".equals(params.param("target-caller"));
    boolean network = "true".equals(params.param("target-network"));
    String filename = params.param("target-filename");
    String storename = params.param("target-store");

    // Channel (from params)
    final TVChannel channel = CoreData.me.myChannels.get(channelid);
    if (channel == null)
      throw new HTTPException(404, "No such channel");
    try {
      if (!card.canRecord(channel))
        throw new HTTPException(404, "This device can't record this channel");
    } catch (Misconfiguration e) {
      throw new HTTPException(400, "Channel on device failed", e);
    }
    Util.debug("TVCardProvider.startRecording: card " + card.id() + ", channel " + channel.id());

    // Already recording?
    // TODO move list of recorders to card
    // TODO support canRecordSimultanously()
    if (myOngoingRecordings.containsKey(card.id())) 
      throw new HTTPException(450, "This TV card is already recording", card.id() + "");
    
    // Save video in local file
    if (filename != null)
    {
      if (filename == null || storename == null)
        throw new HTTPException(400, "Need store and filename for local targets");
      Store store = CoreData.me.myStores.get(storename);
      if (store == null)
        throw new HTTPException(404, "Store not found", storename);
      if (!store.isLocal())
        throw new HTTPException(404, "Store is not here", storename);
      if (!(store instanceof LocalStore))
        throw new HTTPException(500, "Store is here, but not a LocalStore", storename);
      LocalStore lstore = (LocalStore) store;
      Util.debug("local store " + lstore.storeName());
      Target target;
      try {
        target = lstore.saveFile(filename);
      } catch (FileNotFoundException e) {
        throw new HTTPException(404, "Could not open the desired filename", e);        
      }
      Util.debug("target " + target.toString());

      TVRecorder recorder = createRecorder(card, channel);

      try {
        StreamCopy sc = new StreamCopy();
        Util.debug("start copy from " + recorder.inStream() + " to " + target.toString());
        sc.copy(new BufferedInputStream(recorder.inStream()), new BufferedOutputStream(target.outStream()), true, "Recording");
        request.sendResponseHeaders(200, true);
      } catch (Exception e) {
        throw new HTTPException(500, "Recording failed", e);
      }
    }
    // Return video data in outstream of this HTTP connection
    else if (sendToCaller)
    {
      Util.debug("sending video back to caller");

      TVRecorder recorder = createRecorder(card, channel);

      try {
        request.sendResponseHeaders(200, true);
        StreamCopy sc = new StreamCopy();
        Util.debug("start copy from " + recorder.inStream() + " to network");
        sc.copy(new BufferedInputStream(recorder.inStream()), new BufferedOutputStream(request.getResponseBody()), true, "Recording");
        // XXX not endless, but a) end timer and b) react to connection drop
      } catch (Exception e) {
        Util.error(e); // TODO
        e.printStackTrace();
        try {
          recorder.stopRecording();
        } catch (CantUseTVCard e1) {
          Util.error(e1);
          e1.printStackTrace();
        }
      };
    }
    // Offer video stream via URL to the network
    else if (network)
    {
      final RemoteTarget rtarget = new RemoteTarget();
      Util.debug("remote target " + rtarget.toString() + " at URL " + rtarget.url());

      final TVRecorder recorder = createRecorder(card, channel);

      // Respond with stream URL to connect to
      request.getResponseHeaders().put("source-url", rtarget.url().toString());
      request.sendResponseHeaders(200, true);

      // Wait for remote end to connect
      Util.debug("Waiting for remote end to connect");
      rtarget.setConnectedCallback(new Runnable()
      {
        public void run() // runs in thread created for RemoteTarget handling of network request
        {
          try {
            Util.debug("woke up, creating recorder");
            StreamCopy sc = new StreamCopy();
            Util.debug("start copy from " + recorder.inStream() + " to " + rtarget.toString());
            sc.copyBlocking(new BufferedInputStream(recorder.inStream()), new BufferedOutputStream(rtarget.outStream()), true);
            // XXX not endless
            // end timer and listen for stop request
          } catch (Exception e) {
            Util.error(e); // TODO
            e.printStackTrace();
            try {
              recorder.stopRecording();
            } catch (CantUseTVCard e1) {
              Util.error(e1);
              e1.printStackTrace();
            }
          }
        }
      });
    }
    else 
      throw new HTTPException(400, "Target missing or inconsistent, please check parameters");
  }
  protected TVRecorder createRecorder(TVCard card, TVChannel channel)
    throws HTTPException
  {
    try {
      TVRecorder recorder = card.newRecording();
      Util.debug("startRecording on " + recorder.toString());
      recorder.startRecording(channel);
      myOngoingRecordings.put(card.id(), recorder);
      return recorder;
    } catch (Exception e) {
      throw new HTTPException(500, "Recording failed to start", e);
    }
  }

  protected void stopRecording(TVCard card)
    throws HTTPException
  {
    TVRecorder recorder = myOngoingRecordings.get(card.id());
    if (recorder == null)
      throw new HTTPException(450, "This TV card is not recording currently", card.id() + "");
    try {
      Util.debug("stopRecording on " + recorder.toString());
      recorder.stopRecording();
      Util.debug("returned");
    } catch (CantUseTVCard e) {
      throw new HTTPException(500, "Could not stop recording", e);        
    }
    myOngoingRecordings.remove(recorder);
  }
  protected Map<Integer, TVRecorder> myOngoingRecordings;
}
