package org.openni;

import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;

/**
 * The {@link VideoStream} object encapsulates a single video stream from a device. Once created, it
 * is used to start data flow from the device, and to read individual frames of data. This is the
 * central class used to obtain data in OpenNI. It provides the ability to manually read data in a
 * polling loop, as well as providing events and a Listener class that can be used to implement
 * event-driven data acquisition.
 * 
 * Aside from the video data frames themselves, the class offers a number of functions used for
 * obtaining information about a {@link VideoStream}. Field of view, available video modes, and
 * minimum and maximum valid pixel values can all be obtained.
 * 
 * In addition to obtaining data, the {@link VideoStream} object is used to set all configuration
 * properties that apply to a specific stream (rather than to an entire device). In particular, it
 * is used to control cropping, mirroring, and video modes.
 * 
 * A valid, initialized device that provides the desired stream type is required to create a stream.
 * 
 * Several video streams can be created to stream data from the same sensor. This is useful if
 * several components of an application need to read frames separately.
 * 
 * While some device might allow different streams from the same sensor to have different
 * configurations, most devices will have a single configuration for the sensor, shared by all
 * streams.
 */
public class VideoStream {
  /**
   * The {@link CameraSettings} object encapsulates camera setting for a single device. Once
   * created, it is used to get and set auto exposure and auto white balance modes.
   */
  public class CameraSettings {
    /**
     * Set Auto Exposure Enabled for corresponding sensor
     * 
     * @param enabled boolean value for set and unset auto exposure mode
     */
    public void setAutoExposureEnabled(boolean enabled) {
      NativeMethods.checkReturnStatus(NativeMethods.oniStreamSetProperty(mVideoStream.getHandle(),
          NativeMethods.STREAM_PROPERTY_AUTO_EXPOSURE, enabled));
    }

    /**
     * Set Auto White Balance for corresponding sensor
     * 
     * @param enabled boolean value for set and unset auto white balance mode
     */
    public void setAutoWhiteBalanceEnabled(boolean enabled) {
      NativeMethods.checkReturnStatus(NativeMethods.oniStreamSetProperty(mVideoStream.getHandle(),
          NativeMethods.STREAM_PROPERTY_AUTO_WHITE_BALANCE, enabled));
    }

    /**
     * Set Auto Exposure Enabled for corresponding sensor
     * 
     * @return enabled boolean value which define auto exposure mode state
     */
    public boolean getAutoExposureEnabled() {
      OutArg<Boolean> val = new OutArg<Boolean>();
      NativeMethods.oniStreamGetBoolProperty(mVideoStream.getHandle(),
          NativeMethods.STREAM_PROPERTY_AUTO_EXPOSURE, val);
      return val.mValue;
    }

    /**
     * Set Auto White Balance Enabled for corresponding sensor
     * 
     * @return enabled boolean value which define auto white balance mode state
     */
    public boolean getAutoWhiteBalanceEnabled() {
      OutArg<Boolean> val = new OutArg<Boolean>();
      NativeMethods.oniStreamGetBoolProperty(mVideoStream.getHandle(),
          NativeMethods.STREAM_PROPERTY_AUTO_WHITE_BALANCE, val);
      return val.mValue;
    }

    private CameraSettings(VideoStream videoStream) {
      this.mVideoStream = videoStream;
    }

    final private VideoStream mVideoStream;
  }
  /**
   * The VideoStream::NewFrameListener interface is provided to allow the implementation of event
   * driven frame reading. To use it, create a class that inherits from it and implement override
   * the onNewFrame() method. Then, register your created class with an active {@link VideoStream}
   * using the {@link #addNewFrameListener(org.openni.VideoStream.NewFrameListener)} function. Once
   * this is done, the event handler function you implemented will be called whenever a new frame
   * becomes available. You may call {@link org.openni.VideoStream#readFrame()} from within the
   * event handler.
   */
  public interface NewFrameListener {
    /**
     * Derived classes should implement this function to handle new frames.
     */
    public void onFrameReady(VideoStream stream);
  }

  /**
   * Creates a stream of frames from a specific sensor type of a specific device. You must supply a
   * reference to a Device that supplies the sensor type requested. You can use
   * {@link org.openni.Device#hasSensor(SensorType)} to check whether a given sensor is available on
   * your target device before calling create().
   * 
   * @param device A reference to the {@link Device} you want to create the stream on.
   * @param sensorType The type of sensor the stream should produce data from.
   */
  public static VideoStream create(Device device, SensorType sensorType) {
    VideoStream videoStream = new VideoStream(sensorType);
    if (mFrameListeners == null) mFrameListeners = new HashMap<VideoStream, NewFrameListener>();
    NativeMethods.checkReturnStatus(NativeMethods.oniDeviceCreateStream(device.getHandle(),
        sensorType.toNative(), videoStream));
    return videoStream;
  }

  /**
   * Destroy this stream. This function is currently called automatically by the destructor, but it
   * is considered a best practice for applications to manually call this function on any
   * {@link VideoStream} that they call create() for.
   */
  public void destroy() {
    NativeMethods.oniStreamDestroy(getHandle(), mCallbackHandle);
    mStreamHandle = 0;
  }

  /**
   * Provides the {@link SensorInfo} object associated with the sensor that is producing this
   * {@link VideoStream}.
   * 
   * {@link SensorInfo} is useful primarily as a means of learning which video modes are valid for
   * this VideoStream.
   * 
   * @return SensorInfo object associated with the sensor providing this stream.
   */
  public final SensorInfo getSensorInfo() {
    return NativeMethods.oniStreamGetSensorInfo(getHandle());
  }

  /**
   * Starts data generation from this video stream.
   */
  public void start() {
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamStart(getHandle()));
  }

  /**
   * Stops data generation from this video stream.
   */
  public void stop() {
    NativeMethods.oniStreamStop(getHandle());
  }

  /**
   * Read the next frame from this video stream, delivered as a {@link VideoFrameRef}. This is the
   * primary method for manually obtaining frames of video data. If no new frame is available, the
   * call will block until one is available. To avoid blocking, use
   * {@link VideoStream.NewFrameListener} to implement an event driven architecture. Another
   * alternative is to use {@link org.openni.OpenNI#waitForAnyStream(java.util.List, int)} to wait
   * for new frames from several streams
   * 
   * @return VideoFrameRef object which hold the data of the new frame.
   */
  public VideoFrameRef readFrame() {
    OutArg<VideoFrameRef> frame = new OutArg<VideoFrameRef>();
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamReadFrame(getHandle(), frame));
    return frame.mValue;
  }

  /**
   * Adds a new Listener to receive this VideoStream onNewFrame event. See
   * {@link VideoStream.NewFrameListener} for more information on implementing an event driven
   * frame reading architecture.
   * 
   * @param streamListener Object which implements {@link VideoStream.NewFrameListener} that will
   *        respond to this event.
   */
  public void addNewFrameListener(NewFrameListener streamListener) {
    mFrameListeners.put(this, streamListener);
  }

  /**
   * Removes a Listener from this video stream list. The listener removed will no longer receive new
   * frame events from this stream.
   * 
   * @param streamListener Object of the listener to be removed.
   */
  public void removeNewFrameListener(NewFrameListener streamListener) {
    for (Map.Entry<VideoStream, NewFrameListener> pairs : mFrameListeners.entrySet()) {
      VideoStream videoStream = pairs.getKey();
      if (videoStream.getHandle() == mStreamHandle) {
        if (streamListener.equals(pairs.getValue())) {
          mFrameListeners.remove(pairs.getKey());
          return;
        }
      }
    }
  }

  /**
   * This function return stream handle.
   * 
   * @return OpenNI stream handle.
   */
  public long getHandle() {
    return mStreamHandle;
  }

  /**
   * Gets an object through which several camera settings can be configured.
   * 
   * @return null if the stream doesn't support camera settings.
   */
  public CameraSettings getCameraSettings() {
    if (NativeMethods.oniStreamIsPropertySupported(getHandle(),
        NativeMethods.STREAM_PROPERTY_AUTO_EXPOSURE)
        && NativeMethods.oniStreamIsPropertySupported(getHandle(),
            NativeMethods.STREAM_PROPERTY_AUTO_WHITE_BALANCE)) {
      return new CameraSettings(this);
    }
    return null;
  }

  /**
   * Get the current video mode information for this video stream. This includes its resolution, fps
   * and stream format.
   * 
   * @return Current video mode information for this video stream.
   */
  public final VideoMode getVideoMode() {
    OutArg<VideoMode> videoMode = new OutArg<VideoMode>();
    NativeMethods.checkReturnStatus(NativeMethods.getVideoMode(getHandle(), videoMode));
    return videoMode.mValue;
  }

  /**
   * Changes the current video mode of this stream. Recommended practice is to use
   * {@link org.openni.Device#getSensorInfo(SensorType)}, and then
   * {@link org.openni.SensorInfo#getSupportedVideoModes()} to obtain a list of valid video mode
   * settings for this stream. Then, pass a valid {@link VideoMode} to setVideoMode(VideoMode) to
   * ensure correct operation.
   * 
   * @param videoMode Desired new video mode for this stream. returns Status code indicating success
   *        or failure of this operation.
   */
  public void setVideoMode(VideoMode videoMode) {
    NativeMethods.checkReturnStatus(NativeMethods.setVideoMode(getHandle(), videoMode
        .getResolutionX(), videoMode.getResolutionY(), videoMode.getFps(), videoMode
        .getPixelFormat().toNative()));
  }

  /**
   * Provides the maximum possible value for pixels obtained by this stream. This is most useful for
   * getting the maximum possible value of depth streams.
   * 
   * @return Maximum possible pixel value.
   */
  public int getMaxPixelValue() {
    OutArg<Integer> val = new OutArg<Integer>();
    NativeMethods
        .oniStreamGetIntProperty(getHandle(), NativeMethods.STREAM_PROPERTY_MAX_VALUE, val);
    return val.mValue;
  }

  /**
   * Provides the smallest possible value for pixels obtains by this VideoStream. This is most
   * useful for getting the minimum possible value that will be reported by a depth stream.
   * 
   * @return Minimum possible pixel value that can come from this stream.
   */
  public int getMinPixelValue() {
    OutArg<Integer> val = new OutArg<Integer>();
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamGetIntProperty(getHandle(),
        NativeMethods.STREAM_PROPERTY_MIN_VALUE, val));
    return val.mValue;
  }

  /**
   * Checks whether this stream supports cropping.
   * 
   * @return true if the stream supports cropping, false if it does not.
   */
  public boolean isCroppingSupported() {
    return NativeMethods.oniStreamIsPropertySupported(getHandle(),
        NativeMethods.STREAM_PROPERTY_CROPPING);
  }

  /**
   * Obtains the current cropping settings for this stream.
   * 
   * @return CropArea CropArea object which encapsulated cropping info.
   */
  public CropArea getCropping() {
    OutArg<Integer> xRes = new OutArg<Integer>();
    OutArg<Integer> yRes = new OutArg<Integer>();
    OutArg<Integer> w = new OutArg<Integer>();
    OutArg<Integer> h = new OutArg<Integer>();

    NativeMethods.checkReturnStatus(NativeMethods.getCropping(getHandle(), xRes, yRes, w, h));
    return new CropArea(xRes.mValue, yRes.mValue, w.mValue, h.mValue);
  }

  /**
   * Changes the cropping settings for this stream. You can use the {@link #isCroppingSupported()}
   * function to make sure cropping is supported before calling this function.
   * 
   * @param cropping CropArea object which set corresponding cropping information.
   */
  public void setCropping(CropArea cropping) {
    NativeMethods.checkReturnStatus(NativeMethods.setCropping(getHandle(), cropping.getOriginX(),
        cropping.getOriginY(), cropping.getWidth(), cropping.getHeight()));
  }

  /**
   * Disables cropping.
   */
  public void resetCropping() {
    NativeMethods.checkReturnStatus(NativeMethods.resetCropping(getHandle()));
  }

  /**
   * Check whether mirroring is currently turned on for this stream.
   * 
   * @return true if mirroring is currently enabled, false otherwise.
   */
  public boolean getMirroringEnabled() {
    OutArg<Boolean> val = new OutArg<Boolean>();
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamGetBoolProperty(getHandle(),
        NativeMethods.STREAM_PROPERTY_MIRRORING, val));
    return val.mValue;
  }

  /**
   * Enable or disable mirroring for this stream.
   * 
   * @param isEnabled true to enable mirroring, false to disable it.
   */
  public void setMirroringEnabled(boolean isEnabled) {
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamSetProperty(getHandle(),
        NativeMethods.STREAM_PROPERTY_MIRRORING, isEnabled));
  }

  /**
   * Gets the horizontal field of view of frames received from this stream.
   * 
   * @return Horizontal field of view, in radians.
   */
  public float getHorizontalFieldOfView() {
    OutArg<Float> val = new OutArg<Float>();
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamGetFloatProperty(getHandle(),
        NativeMethods.STREAM_PROPERTY_HORIZONTAL_FOV, val));
    return val.mValue;
  }

  /**
   * Gets the vertical field of view of frames received from this stream.
   * 
   * @return Vertical field of view, in radians.
   */
  public float getVerticalFieldOfView() {
    OutArg<Float> val = new OutArg<Float>();
    NativeMethods.checkReturnStatus(NativeMethods.oniStreamGetFloatProperty(getHandle(),
        NativeMethods.STREAM_PROPERTY_VERTICAL_FOV, val));
    return val.mValue;
  }

  /**
   * Gets the sensor type for this stream.
   * 
   * @return sensor type.
   */
  public SensorType getSensorType() {
    return mSensorType;
  }

  private VideoStream(SensorType sensorType) {
    this.mSensorType = sensorType;
  }

  private static void onFrameReady(long streamHandle) {
    for (Map.Entry<VideoStream, NewFrameListener> pairs : mFrameListeners.entrySet()) {
      VideoStream videoStream = pairs.getKey();
      if (videoStream.getHandle() == streamHandle) {
        pairs.getValue().onFrameReady(videoStream);
      }
    }

  }

  private final SensorType mSensorType;
  private static HashMap<VideoStream, NewFrameListener> mFrameListeners;
  private long mStreamHandle;
  private long mCallbackHandle;
}
