/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.hardware;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.io.IOException;

import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;

/**
 * The Camera class is used to connect/disconnect with the camera service,
 * set capture settings, start/stop preview, snap a picture, and retrieve
 * frames for encoding for video.
 * <p>There is no default constructor for this class. Use {@link #open()} to
 * get a Camera object.</p>
 */
public class Camera {
    private static final String TAG = "Camera";
    
    // These match the enums in frameworks/base/include/ui/Camera.h
    private static final int CAMERA_MSG_ERROR = 0;
    private static final int CAMERA_MSG_SHUTTER = 1;
    private static final int CAMERA_MSG_FOCUS = 2;
    private static final int CAMERA_MSG_ZOOM = 3;
    private static final int CAMERA_MSG_PREVIEW_FRAME = 4;
    private static final int CAMERA_MSG_VIDEO_FRAME = 5;
    private static final int CAMERA_MSG_POSTVIEW_FRAME = 6;
    private static final int CAMERA_MSG_RAW_IMAGE = 7;
    private static final int CAMERA_MSG_COMPRESSED_IMAGE = 8;

    private int mNativeContext; // accessed by native methods
    private EventHandler mEventHandler;
    private ShutterCallback mShutterCallback;
    private PictureCallback mRawImageCallback;
    private PictureCallback mJpegCallback;
    private PreviewCallback mPreviewCallback;
    private AutoFocusCallback mAutoFocusCallback;
    private ErrorCallback mErrorCallback;
    private boolean mOneShot;
    
    /**
     * Returns a new Camera object.
     */
    public static Camera open() { 
        return new Camera(); 
    }

    Camera() {
        mShutterCallback = null;
        mRawImageCallback = null;
        mJpegCallback = null;
        mPreviewCallback = null;

        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            mEventHandler = new EventHandler(this, looper);
        } else {
            mEventHandler = null;
        }

        native_setup(new WeakReference<Camera>(this));
    }
    
    protected void finalize() { 
        native_release(); 
    }
    
    private native final void native_setup(Object camera_this);
    private native final void native_release();
    

    /**
     * Disconnects and releases the Camera object resources.
     * <p>It is recommended that you call this as soon as you're done with the 
     * Camera object.</p>
     */
    public final void release() { 
        native_release();
    }

    /**
     * Reconnect to the camera after passing it to MediaRecorder. To save
     * setup/teardown time, a client of Camera can pass an initialized Camera
     * object to a MediaRecorder to use for video recording. Once the 
     * MediaRecorder is done with the Camera, this method can be used to
     * re-establish a connection with the camera hardware. NOTE: The Camera
     * object must first be unlocked by the process that owns it before it
     * can be connected to another proces.
     *
     * @throws IOException if the method fails.
     *
     * FIXME: Unhide after approval
     * @hide
     */
    public native final void reconnect() throws IOException;
    
    /**
     * Lock the camera to prevent other processes from accessing it. To save
     * setup/teardown time, a client of Camera can pass an initialized Camera
     * object to another process. This method is used to re-lock the Camera
     * object prevent other processes from accessing it. By default, the
     * Camera object is locked. Locking it again from the same process will
     * have no effect. Attempting to lock it from another process if it has
     * not been unlocked will fail.
     * Returns 0 if lock was successful.
     *
     * FIXME: Unhide after approval
     * @hide
     */
    public native final int lock();
    
    /**
     * Unlock the camera to allow aother process to access it. To save
     * setup/teardown time, a client of Camera can pass an initialized Camera
     * object to another process. This method is used to unlock the Camera
     * object before handing off the Camera object to the other process.

     * Returns 0 if unlock was successful.
     *
     * FIXME: Unhide after approval
     * @hide
     */
    public native final int unlock();
    
    /**
     * Sets the SurfaceHolder to be used for a picture preview. If the surface
     * changed since the last call, the screen will blank. Nothing happens
     * if the same surface is re-set.
     * 
     * @param holder the SurfaceHolder upon which to place the picture preview
     * @throws IOException if the method fails.
     */
    public final void setPreviewDisplay(SurfaceHolder holder) throws IOException {
        if (holder != null) {
            setPreviewDisplay(holder.getSurface());
        } else {
            setPreviewDisplay((Surface)null);
        }
    }

    private native final void setPreviewDisplay(Surface surface);

    /**
     * Used to get a copy of each preview frame.
     */
    public interface PreviewCallback
    {
        /**
         * The callback that delivers the preview frames.
         *
         * @param data The contents of the preview frame in getPreviewFormat()
         *             format.
         * @param camera The Camera service object.
         */
        void onPreviewFrame(byte[] data, Camera camera);
    };
    
    /**
     * Start drawing preview frames to the surface.
     */
    public native final void startPreview();
    
    /**
     * Stop drawing preview frames to the surface.
     */
    public native final void stopPreview();
    
    /**
     * Return current preview state.
     *
     * FIXME: Unhide before release
     * @hide
     */
    public native final boolean previewEnabled();
    
    /**
     * Can be called at any time to instruct the camera to use a callback for
     * each preview frame in addition to displaying it.
     *
     * @param cb A callback object that receives a copy of each preview frame.
     *           Pass null to stop receiving callbacks at any time.
     */
    public final void setPreviewCallback(PreviewCallback cb) {
        mPreviewCallback = cb;
        mOneShot = false;
        setHasPreviewCallback(cb != null, false);
    }

    /**
     * Installs a callback to retrieve a single preview frame, after which the
     * callback is cleared.
     *
     * @param cb A callback object that receives a copy of the preview frame.
     */
    public final void setOneShotPreviewCallback(PreviewCallback cb) {
        if (cb != null) {
            mPreviewCallback = cb;
            mOneShot = true;
            setHasPreviewCallback(true, true);
        }
    }

    private native final void setHasPreviewCallback(boolean installed, boolean oneshot);

    private class EventHandler extends Handler
    {
        private Camera mCamera;

        public EventHandler(Camera c, Looper looper) {
            super(looper);
            mCamera = c;
        }

        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
            case CAMERA_MSG_SHUTTER:
                if (mShutterCallback != null) {
                    mShutterCallback.onShutter();
                }
                return;

            case CAMERA_MSG_RAW_IMAGE:
                if (mRawImageCallback != null)
                    mRawImageCallback.onPictureTaken((byte[])msg.obj, mCamera);
                return;

            case CAMERA_MSG_COMPRESSED_IMAGE:
                if (mJpegCallback != null)
                    mJpegCallback.onPictureTaken((byte[])msg.obj, mCamera);
                return;
            
            case CAMERA_MSG_PREVIEW_FRAME:
                if (mPreviewCallback != null) {
                    mPreviewCallback.onPreviewFrame((byte[])msg.obj, mCamera);
                    if (mOneShot) {
                        mPreviewCallback = null;
                    }
                }
                return;

            case CAMERA_MSG_FOCUS:
                if (mAutoFocusCallback != null)
                    mAutoFocusCallback.onAutoFocus(msg.arg1 == 0 ? false : true, mCamera);
                return;

            case CAMERA_MSG_ERROR :
                Log.e(TAG, "Error " + msg.arg1);
                if (mErrorCallback != null)
                    mErrorCallback.onError(msg.arg1, mCamera);
                return;

            default:
                Log.e(TAG, "Unknown message type " + msg.what);
                return;
            }
        }
    }

    private static void postEventFromNative(Object camera_ref,
                                            int what, int arg1, int arg2, Object obj)
    {
        Camera c = (Camera)((WeakReference)camera_ref).get();
        if (c == null)
            return;

        if (c.mEventHandler != null) {
            Message m = c.mEventHandler.obtainMessage(what, arg1, arg2, obj);
            c.mEventHandler.sendMessage(m);
        }
    }

    /**
     * Handles the callback for the camera auto focus.
     */
    public interface AutoFocusCallback
    {
        /**
         * Callback for the camera auto focus.
         * 
         * @param success true if focus was successful, false if otherwise
         * @param camera  the Camera service object
         */
        void onAutoFocus(boolean success, Camera camera);
    };

    /**
     * Starts auto-focus function and registers a callback function to
     * run when camera is focused. Only valid after startPreview() has
     * been called.
     * 
     * @param cb the callback to run
     */
    public final void autoFocus(AutoFocusCallback cb)
    {
        mAutoFocusCallback = cb;
        native_autoFocus();
    }
    private native final void native_autoFocus();

    /**
     * An interface which contains a callback for the shutter closing after taking a picture.
     */
    public interface ShutterCallback
    {
        /**
         * Can be used to play a shutter sound as soon as the image has been captured, but before
         * the data is available.
         */
        void onShutter();
    }

    /**
     * Handles the callback for when a picture is taken.
     */
    public interface PictureCallback {
        /**
         * Callback for when a picture is taken.
         * 
         * @param data   a byte array of the picture data
         * @param camera the Camera service object
         */
        void onPictureTaken(byte[] data, Camera camera);
    };

    /**
     * Triggers an asynchronous image capture. The camera service
     * will initiate a series of callbacks to the application as the
     * image capture progresses. The shutter callback occurs after
     * the image is captured. This can be used to trigger a sound
     * to let the user know that image has been captured. The raw
     * callback occurs when the raw image data is available. The jpeg
     * callback occurs when the compressed image is available. If the
     * application does not need a particular callback, a null can be
     * passed instead of a callback method.
     * 
     * @param shutter   callback after the image is captured, may be null
     * @param raw       callback with raw image data, may be null
     * @param jpeg      callback with jpeg image data, may be null
     */
    public final void takePicture(ShutterCallback shutter, PictureCallback raw,
            PictureCallback jpeg) {
        mShutterCallback = shutter;
        mRawImageCallback = raw;
        mJpegCallback = jpeg;
        native_takePicture();
    }
    private native final void native_takePicture();

    // These match the enum in include/ui/Camera.h
    /** Unspecified camerar error.  @see #ErrorCallback */
    public static final int CAMERA_ERROR_UNKNOWN = 1;
    /** Media server died. In this case, the application must release the
     * Camera object and instantiate a new one. @see #ErrorCallback */
    public static final int CAMERA_ERROR_SERVER_DIED = 100;
    
    /**
     * Handles the camera error callback.
     */
    public interface ErrorCallback
    {
        /**
         * Callback for camera errors.
         * @param error   error code:
         * <ul>
         * <li>{@link #CAMERA_ERROR_UNKNOWN}
         * <li>{@link #CAMERA_ERROR_SERVER_DIED}
         * </ul>
         * @param camera  the Camera service object
         */
        void onError(int error, Camera camera);
    };

    /**
     * Registers a callback to be invoked when an error occurs.
     * @param cb the callback to run
     */
    public final void setErrorCallback(ErrorCallback cb)
    {
        mErrorCallback = cb;
    }
    
    private native final void native_setParameters(String params);
    private native final String native_getParameters();

    /**
     * Sets the Parameters for pictures from this Camera service.
     * 
     * @param params the Parameters to use for this Camera service
     */
    public void setParameters(Parameters params) {
        native_setParameters(params.flatten());
    }

    /**
     * Returns the picture Parameters for this Camera service.
     */
    public Parameters getParameters() {
        Parameters p = new Parameters();
        String s = native_getParameters();
        p.unflatten(s);
        return p;
    }

    /**
     * Handles the picture size (dimensions).
     */
    public class Size {
        /**
         * Sets the dimensions for pictures.
         * 
         * @param w the photo width (pixels)
         * @param h the photo height (pixels)
         */
        public Size(int w, int h) {
            width = w;
            height = h;
        }
        /** width of the picture */
        public int width;
        /** height of the picture */
        public int height;
    };

    /**
     * Handles the parameters for pictures created by a Camera service.
     */
    public class Parameters {
        private HashMap<String, String> mMap;

        private Parameters() {
            mMap = new HashMap<String, String>();
        }

        /**
         * Writes the current Parameters to the log.
         * @hide
         * @deprecated
         */
        public void dump() {
            Log.e(TAG, "dump: size=" + mMap.size());
            for (String k : mMap.keySet()) {
                Log.e(TAG, "dump: " + k + "=" + mMap.get(k));
            }
        }

        /**
         * Creates a single string with all the parameters set in
         * this Parameters object.
         * <p>The {@link #unflatten(String)} method does the reverse.</p>
         * 
         * @return a String with all values from this Parameters object, in
         *         semi-colon delimited key-value pairs
         */
        public String flatten() {
            StringBuilder flattened = new StringBuilder();
            for (String k : mMap.keySet()) {
                flattened.append(k);
                flattened.append("=");
                flattened.append(mMap.get(k));
                flattened.append(";");
            }
            // chop off the extra semicolon at the end
            flattened.deleteCharAt(flattened.length()-1);
            return flattened.toString();
        }

        /**
         * Takes a flattened string of parameters and adds each one to 
         * this Parameters object.
         * <p>The {@link #flatten()} method does the reverse.</p>
         * 
         * @param flattened a String of parameters (key-value paired) that 
         *                  are semi-colon delimited
         */
        public void unflatten(String flattened) {
            mMap.clear();
            
            StringTokenizer tokenizer = new StringTokenizer(flattened, ";");
            while (tokenizer.hasMoreElements()) {
                String kv = tokenizer.nextToken();
                int pos = kv.indexOf('=');
                if (pos == -1) {
                    continue;
                }
                String k = kv.substring(0, pos);
                String v = kv.substring(pos + 1);
                mMap.put(k, v);
            }
        }
        
        public void remove(String key) {
            mMap.remove(key);
        }

        /**
         * Sets a String parameter.
         * 
         * @param key   the key name for the parameter
         * @param value the String value of the parameter
         */
        public void set(String key, String value) {
            if (key.indexOf('=') != -1 || key.indexOf(';') != -1) {
                Log.e(TAG, "Key \"" + key + "\" contains invalid character (= or ;)");
                return;
            }
            if (value.indexOf('=') != -1 || value.indexOf(';') != -1) {
                Log.e(TAG, "Value \"" + value + "\" contains invalid character (= or ;)");
                return;
            }

            mMap.put(key, value);
        }

        /**
         * Sets an integer parameter.
         * 
         * @param key   the key name for the parameter
         * @param value the int value of the parameter
         */
        public void set(String key, int value) {
            mMap.put(key, Integer.toString(value));
        }

        /**
         * Returns the value of a String parameter.
         * 
         * @param key the key name for the parameter
         * @return the String value of the parameter
         */
        public String get(String key) {
            return mMap.get(key);
        }

        /**
         * Returns the value of an integer parameter.
         * 
         * @param key the key name for the parameter
         * @return the int value of the parameter
         */
        public int getInt(String key) {
            return Integer.parseInt(mMap.get(key));
        }

        /**
         * Sets the dimensions for preview pictures.
         * 
         * @param width  the width of the pictures, in pixels
         * @param height the height of the pictures, in pixels
         */
        public void setPreviewSize(int width, int height) {
            String v = Integer.toString(width) + "x" + Integer.toString(height);
            set("preview-size", v);
        }

        /**
         * Returns the dimensions setting for preview pictures.
         * 
         * @return a Size object with the height and width setting 
         *          for the preview picture
         */
        public Size getPreviewSize() {
            String pair = get("preview-size");
            if (pair == null)
                return null;
            String[] dims = pair.split("x");
            if (dims.length != 2)
                return null;

            return new Size(Integer.parseInt(dims[0]),
                            Integer.parseInt(dims[1]));

        }

        /**
         * Sets the dimensions for EXIF thumbnails.
         * 
         * @param width  the width of the thumbnail, in pixels
         * @param height the height of the thumbnail, in pixels
         *
         * FIXME: unhide before release
         * @hide
         */
        public void setThumbnailSize(int width, int height) {
            set("jpeg-thumbnail-width", width);
            set("jpeg-thumbnail-height", height);
        }

        /**
         * Returns the dimensions for EXIF thumbnail
         * 
         * @return a Size object with the height and width setting 
         *          for the EXIF thumbnails
         *
         * FIXME: unhide before release
         * @hide
         */
        public Size getThumbnailSize() {
            return new Size(getInt("jpeg-thumbnail-width"),
                            getInt("jpeg-thumbnail-height"));
        }

        /**
         * Sets the quality of the EXIF thumbnail
         * 
         * @param quality the JPEG quality of the EXIT thumbnail
         *
         * FIXME: unhide before release
         * @hide
         */
        public void setThumbnailQuality(int quality) {
            set("jpeg-thumbnail-quality", quality);
        }

        /**
         * Returns the quality setting for the EXIF thumbnail
         * 
         * @return the JPEG quality setting of the EXIF thumbnail
         *
         * FIXME: unhide before release
         * @hide
         */
        public int getThumbnailQuality() {
            return getInt("jpeg-thumbnail-quality");
        }

        /**
         * Sets the rate at which preview frames are received.
         * 
         * @param fps the frame rate (frames per second)
         */
        public void setPreviewFrameRate(int fps) {
            set("preview-frame-rate", fps);
        }

        /**
         * Returns the setting for the rate at which preview frames
         * are received.
         * 
         * @return the frame rate setting (frames per second)
         */
        public int getPreviewFrameRate() {
            return getInt("preview-frame-rate");
        }

        /**
         * Sets the image format for preview pictures.
         * 
         * @param pixel_format the desired preview picture format 
         *                     (<var>PixelFormat.YCbCr_420_SP</var>,
         *                      <var>PixelFormat.RGB_565</var>, or
         *                      <var>PixelFormat.JPEG</var>)
         * @see android.graphics.PixelFormat
         */
        public void setPreviewFormat(int pixel_format) {
            String s = cameraFormatForPixelFormat(pixel_format);
            if (s == null) {
                throw new IllegalArgumentException();
            }

            set("preview-format", s);
        }

        /**
         * Returns the image format for preview pictures.
         * 
         * @return the PixelFormat int representing the preview picture format
         */
        public int getPreviewFormat() {
            return pixelFormatForCameraFormat(get("preview-format"));
        }

        /**
         * Sets the dimensions for pictures.
         * 
         * @param width  the width for pictures, in pixels
         * @param height the height for pictures, in pixels
         */
        public void setPictureSize(int width, int height) {
            String v = Integer.toString(width) + "x" + Integer.toString(height);
            set("picture-size", v);
        }

        /**
         * Returns the dimension setting for pictures.
         * 
         * @return a Size object with the height and width setting 
         *          for pictures
         */
        public Size getPictureSize() {
            String pair = get("picture-size");
            if (pair == null)
                return null;
            String[] dims = pair.split("x");
            if (dims.length != 2)
                return null;

            return new Size(Integer.parseInt(dims[0]),
                            Integer.parseInt(dims[1]));

        }

        /**
         * Sets the image format for pictures.
         * 
         * @param pixel_format the desired picture format 
         *                     (<var>PixelFormat.YCbCr_420_SP</var>,
         *                      <var>PixelFormat.RGB_565</var>, or
         *                      <var>PixelFormat.JPEG</var>)
         * @see android.graphics.PixelFormat
         */
        public void setPictureFormat(int pixel_format) {
            String s = cameraFormatForPixelFormat(pixel_format);
            if (s == null) {
                throw new IllegalArgumentException();
            }

            set("picture-format", s);
        }

        /**
         * Returns the image format for pictures.
         * 
         * @return the PixelFormat int representing the picture format
         */
        public int getPictureFormat() {
            return pixelFormatForCameraFormat(get("picture-format"));
        }

        private String cameraFormatForPixelFormat(int pixel_format) {
            switch(pixel_format) {
            case PixelFormat.YCbCr_422_SP: return "yuv422sp";
            case PixelFormat.YCbCr_420_SP: return "yuv420sp";
            case PixelFormat.RGB_565:      return "rgb565";
            case PixelFormat.JPEG:         return "jpeg";
            default:                       return null;
            }
        }

        private int pixelFormatForCameraFormat(String format) {
            if (format == null)
                return PixelFormat.UNKNOWN;

            if (format.equals("yuv422sp"))
                return PixelFormat.YCbCr_422_SP;

            if (format.equals("yuv420sp"))
                return PixelFormat.YCbCr_420_SP;

            if (format.equals("rgb565"))
                return PixelFormat.RGB_565;

            if (format.equals("jpeg"))
                return PixelFormat.JPEG;

            return PixelFormat.UNKNOWN;
        }

    };
}


