/*
 * This test program largely adapted from the android SDK camera preview
 * example which has this copyright:
 *
 * Copyright (C) 2007 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 lan.my.zooty.andyscan;

import android.content.Context;
import android.hardware.Camera;
import android.hardware.Camera.Size;
import android.hardware.Camera.PreviewCallback;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.util.AttributeSet;
import android.widget.TextView;
import android.view.MotionEvent;
import android.graphics.ImageFormat;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;

import java.io.IOException;
import java.util.List;
import java.lang.Thread;

/**
 * A simple wrapper around a Camera and a SurfaceView that renders a
 * centered preview of the Camera to the surface. We need to center the
 * SurfaceView because not all devices have cameras that support preview
 * sizes at the same aspect ratio as the device's display.
 */
class Preview extends ViewGroup implements SurfaceHolder.Callback {
   private final String TAG = "Preview";

   SurfaceView mSurfaceView;
   SurfaceHolder mHolder;
   Size mPreviewSize;
   List<Size> mSupportedPreviewSizes;
   Camera mCamera;
   byte[] lastBuffer = null;
   TextView lightText;
   TextView evText;
   TextView motionText;
   TextView msgText;
   int afseq = 1;
   int touch_count = 0;
   boolean drain_buffers = false;
   int drain_buffer_count = 2;
   SharedPreferences prefs;

   // This custom PreviewCallback contains the code to do both light level
   // detection and motion detection.
   //
   // The light detection adds up the Y part of the YUV preview image to get
   // an idea of how bright things are in front of the camera, it is a
   // little complicated to use in practice because the camera adapts the
   // exposure setting and the computed light level drops in the preview
   // even if the actual light level stays high in the real field of
   // view. You'd think I could just read the exposure setting to get an
   // idea of the light level, but no android device I've tried this code
   // with has ever reported anything other than zero as the exposure value,
   // so I have to go with watching the change in luminance for detecting
   // light.
   //
   // To do motion detection, I arrange to compare one preview image
   // (again, just the black & white bits) with the previous preview image,
   // adding up the differences between each pixel. A large overall difference
   // means things are moving around, a small one means things are
   // relatively still (at least that is the theory).
   //
   // To do all this, I need preview buffers that won't be stepped on, so
   // I allocate two of them and only call addCallbackBuffer() to put back
   // the oldest buffer when I am through with it.
   //
   // Some tricky stuff goes on with these previews. Experimental evidence
   // seems to indicate that while I have this buffered callback running and
   // have buffers defined that calling takePicture() does not work worth
   // spit (I never seem to get the jpegCallback), so when I want to take a
   // picture I have to drain the buffers by going through a copuple of
   // dummy callbacks where I don't re-add the buffers, then after the
   // buffers are well and truly drained, activate a one shot preview
   // callback so I'll know preview is running normally again without
   // preview buffers, then I can finally take the picture and start the
   // buffer previews back up again after the the picture taking is
   // finished. This technique was rather painfully developed by trail and
   // error, and it may be overkill, but since none of these screwy
   // interactions seem to be documented, I'm going with the first technique
   // that actually allowed takePicture() to function correctly (whew!).

   PreviewCallback mPreviewCallback = new PreviewCallback() {
      @Override
      public void onPreviewFrame(byte[] data, Camera camera) {
         if (drain_buffers) {
            if (lastBuffer != null) {
               drain_buffer_count--;
               lastBuffer = null;
            }
            drain_buffer_count--;
            if (drain_buffer_count <= 0) {
               // post the one shot callback call
               Log.i(TAG,"buffers drained, posting one shot");
               post(new Runnable() {
                  public void run() {
                     mCamera.setOneShotPreviewCallback(mOneShotCallback);
                  }
               });
            }
            return;
         }
         int byteCount = mPreviewSize.width * mPreviewSize.height;
         if (lightText != null) {
            int luma = 0;
            for (int i = 0; i < byteCount; ++i) {
               luma += (((int)(data[i])) & 0xff);
            }
            String lumastr = "" + luma;
            lightText.setText((CharSequence)lumastr);
            if ((evText != null) && (mCamera != null)) {
               Camera.Parameters p = mCamera.getParameters();
               int evcomp = p.getExposureCompensation();
               float evstep = p.getExposureCompensationStep();
               int minev = p.getMinExposureCompensation();
               int maxev = p.getMaxExposureCompensation();
               if (minev != maxev) {
                  evstep = evstep * (float)evcomp;
                  String evstr = Float.toString(evstep);
                  evText.setText((CharSequence)evstr);
               }
            }
         }
         if ((lastBuffer != null) && (motionText != null)) {
            int diff = 0;
            for (int j = 0; j < byteCount; ++j) {
               int d = (((int)(data[j])) & 0xff) -
                     (((int)(lastBuffer[j])) & 0xff);
               if (d < 0) {
                  d = - d;
               }
               diff += d;
            }
            String motionstr = "" + diff;
            motionText.setText((CharSequence)motionstr);
            if (mCamera != null) {
               mCamera.addCallbackBuffer(lastBuffer);
            }
         }
         lastBuffer = data;
      }
   };

   PreviewCallback mOneShotCallback = new PreviewCallback() {
      @Override
      public void onPreviewFrame(byte[] data, Camera camera) {
         Log.i(TAG,"in one shot, posting takePicture");
         SharedPreferences prefs = getPrefs();
         String delaystr = prefs.getString("trigger_edit_key", "0.0");
         float delaysec = Float.parseFloat(delaystr);
         long delayms = (long)((1000.0 * delaysec) + 0.5);
         postDelayed(new Runnable() {
            public void run() {
               mCamera.takePicture(shutCallback, rawCallback,
                                   postCallback, jpegCallback);
            }
         }, delayms);
         return;
      }
   };

   public
   Preview(Context context, AttributeSet attrs) {
      super(context, attrs);

      mSurfaceView = new SurfaceView(context);
      addView(mSurfaceView);

      // Install a SurfaceHolder.Callback so we get notified when the
      // underlying surface is created and destroyed.
      mHolder = mSurfaceView.getHolder();
      mHolder.addCallback(this);
      mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }

   public void setCamera(Camera camera) {
      mCamera = camera;
      if (mCamera != null) {
         mSupportedPreviewSizes = mCamera.getParameters().getSupportedPreviewSizes();
         requestLayout();
      }
   }

   public void setLightText(TextView newText, TextView newEv) {
      lightText = newText;
      evText = newEv;
   }

   public void setMotionText(TextView newText) {
      motionText = newText;
   }

   public void setMsgText(TextView newText) {
      msgText = newText;
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      // We purposely disregard child measurements because act as a
      // wrapper to a SurfaceView that centers the camera preview instead
      // of stretching it.
      final int width = resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
      final int height = resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
      setMeasuredDimension(width, height);

      if (mSupportedPreviewSizes != null) {
         mPreviewSize = getOptimalPreviewSize(mSupportedPreviewSizes, width, height);
      }
   }

   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
      if (changed && getChildCount() > 0) {
         final View child = getChildAt(0);

         final int width = r - l;
         final int height = b - t;

         int previewWidth = width;
         int previewHeight = height;
         if (mPreviewSize != null) {
            previewWidth = mPreviewSize.width;
            previewHeight = mPreviewSize.height;
         }

         // Center the child SurfaceView within the parent.
         if (width * previewHeight > height * previewWidth) {
            final int scaledChildWidth = previewWidth * height / previewHeight;
            child.layout((width - scaledChildWidth) / 2, 0,
                  (width + scaledChildWidth) / 2, height);
         } else {
            final int scaledChildHeight = previewHeight * width / previewWidth;
            child.layout(0, (height - scaledChildHeight) / 2,
                  width, (height + scaledChildHeight) / 2);
         }
      }
   }

   private Size getOptimalPreviewSize(List<Size> sizes, int w, int h) {
      final double ASPECT_TOLERANCE = 0.1;
      double targetRatio = (double) w / h;
      if (sizes == null) return null;

      Size optimalSize = null;
      double minDiff = Double.MAX_VALUE;

      int targetHeight = h;

      // Hack to see if 640x480 preview works better for me...
      for (Size size : sizes) {
         if (((size.width == 640) && (size.height == 480)) ||
            ((size.width == 480) && (size.height == 640))) {
            return size;
         }
      }

      // Try to find an size match aspect ratio and size
      for (Size size : sizes) {
         double ratio = (double) size.width / size.height;
         if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue;
         if (Math.abs(size.height - targetHeight) < minDiff) {
            optimalSize = size;
            minDiff = Math.abs(size.height - targetHeight);
         }
      }

      // Cannot find the one match the aspect ratio, ignore the requirement
      if (optimalSize == null) {
         minDiff = Double.MAX_VALUE;
         for (Size size : sizes) {
            if (Math.abs(size.height - targetHeight) < minDiff) {
               optimalSize = size;
               minDiff = Math.abs(size.height - targetHeight);
            }
         }
      }
      return optimalSize;
   }

   public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
      // Now that the size is known, set up the camera parameters and begin
      // the preview.
      Camera.Parameters parameters = mCamera.getParameters();
      parameters.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
      requestLayout();

      // These don't exist in the old version of android on my phone :-(.
      //
      // if (parameters.isAutoExposureLockSupported()) {
      //   parameters.setAutoExposureLock(true);
      // }
      // if (parameters.isAutoWhiteBalanceLockSupported()) {
      //   parameters.setAutoWhiteBalanceLock(true);
      // }

      // We are always going to want to take the highest resolution
      // pictures possible, so find the largest size image we can
      // support and set that as picture size.

      List<Size> picsizes = parameters.getSupportedPictureSizes();

      int pxlsize = 0;
      for (Size size : picsizes) {
         int onesize = size.height * size.width;
         if (onesize > pxlsize) {
            pxlsize = onesize;
            Log.i(TAG, "size set to " + size.width + " " + size.height);
            parameters.setPictureSize(size.width, size.height);
         }
      }

      // Found these on the internet:
      // https://groups.google.com/forum/?fromgroups=#!topic/android-developers/Jp909pCYo9U

      parameters.setPictureFormat(ImageFormat.JPEG);
      parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
      parameters.setJpegQuality(100);

      mCamera.setParameters(parameters);
      mCamera.startPreview();
      mCamera.autoFocus(new Camera.AutoFocusCallback(){
            @Override
            public void onAutoFocus(boolean success, Camera camera) {
               Log.d(TAG, "isAutofoucs " + Boolean.toString(success));
            }
         } );
      activateCallbackBuffers();
   }

   public void activateCallbackBuffers() {

      // PUT CODE HERE - no need for callback if trigger isn't light or motion.
      // Add code to check prefs.

      drain_buffers = false;
      mCamera.addCallbackBuffer(
         new byte[2 * mPreviewSize.width * mPreviewSize.height]);
      mCamera.addCallbackBuffer(
         new byte[2 * mPreviewSize.width * mPreviewSize.height]);
      drain_buffer_count = 2;
      mCamera.setPreviewCallbackWithBuffer(mPreviewCallback);
   }

   public void surfaceCreated(SurfaceHolder holder) {
      // The Surface has been created, acquire the camera and tell it where
      // to draw.
      try {
         if (mCamera != null) {
            mCamera.setPreviewDisplay(holder);
         }
      } catch (IOException exception) {
         Log.e(TAG, "IOException caused by setPreviewDisplay()", exception);
      }
   }

   public void surfaceDestroyed(SurfaceHolder holder) {
      // Surface will be destroyed when we return, so stop the preview.
      if (mCamera != null) {
         mCamera.stopPreview();
      }
   }

   Camera.ShutterCallback shutCallback = new Camera.ShutterCallback() {
         public void onShutter() {
            Log.i(TAG, "shutCallback called");
         }
      };

   Camera.PictureCallback rawCallback = new Camera.PictureCallback() {
         public void onPictureTaken(byte[] data, Camera camera) {
            if (data == null) {
               Log.i(TAG, "rawCallback called with null data");
            } else {
               Log.i(TAG, "rawCallback called with " + data.length + " bytes.");
            }
         }
      };

   Camera.PictureCallback postCallback = new Camera.PictureCallback() {
         public void onPictureTaken(byte[] data, Camera camera) {
            if (data == null) {
               Log.i(TAG, "postCallback called with null data");
            } else {
               Log.i(TAG, "postCallback called with " + data.length + " bytes.");
            }
         }
      };

   Camera.PictureCallback jpegCallback = new Camera.PictureCallback() {
         public void onPictureTaken(byte[] data, Camera camera) {
            Log.i(TAG, "jpegCallback called with " + data.length + " bytes.");

            // PUT CODE HERE - accessing the preferences and updating them
            // here is kind of slow. I should probably see about keeping
            // copies of the stuff and updating it only in onResume()
            // and maybe spawing a thread to do the edit of the new
            // page number.

            SharedPreferences prefs = getPrefs();
            String url = prefs.getString("scan_host_edit_key", "");

            String fmt = prefs.getString("image_name_edit_key",
               getResources().getString(R.string.image_name_default));
            String page = prefs.getString("image_number_edit_key",
               getResources().getString(R.string.image_number_default));
            String pinc = prefs.getString("number_inc_edit_key",
               getResources().getString(R.string.number_inc_default));
            boolean do_up = prefs.getBoolean("scan_host_checkbox_key", false);
            boolean do_sd = prefs.getBoolean("local_dir_checkbox_key", false);
            if (! fmt.contains("%")) {
               fmt = fmt + "%04d";
            }
            fmt = fmt + ".jpg";
            int pagenum = Integer.parseInt(page);
            int pageinc = Integer.parseInt(pinc);

            String fname = String.format(fmt,pagenum);
            pagenum = pagenum + pageinc;
            if (pagenum < 0) {
               pagenum = 0;
            }
            page = String.valueOf(pagenum);
            SharedPreferences.Editor ed = prefs.edit();
            ed.putString("image_number_edit_key", page);
            ed.apply();

            // PUT CODE HERE - should also get a rotation to record in the
            // jpeg file so the images will have the proper orientation
            // when they arrived at the server. (Kinda tricky, no good
            // interfaces available for modifying jpeg buffers, only files).

            if (do_up) {
               (new Thread(new PictureUploader(url,fname,data))).start();
            }
            if (do_sd) {
               String locname = prefs.getString("local_dir_edit_key", "");
               if (locname.length() > 0) {
                  locname = locname + "/" + fname;
                  (new Thread(new PictureSaver(locname,data))).start();
               }
            }
            post(new Runnable() {
               public void run() {
                  mCamera.startPreview();
                  activateCallbackBuffers();
               }
            });
         }
      };

   // Just some debug code to prove I can notice screen touch events
   // to trigger taking a photo.

   @Override
   public boolean onTouchEvent(MotionEvent event) {
      if (event.getAction() == MotionEvent.ACTION_DOWN) {
         if (msgText != null) {
            ++touch_count;
            String tme = "You touched me: " + touch_count;
            msgText.setText((CharSequence)tme);
            if (mCamera != null) {
               Log.i(TAG, "in touch event, starting buffer drain");

               // Start the long drawn out process of making it safe to
               // take pictures (sheesh!).

               post(new Runnable() {
                  public void run() {
                     drain_buffers = true;
                  }
               });
            }
         }
      }
      return false;
   }

   SharedPreferences
   getPrefs() {
      if (prefs == null) {
         prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
      }
      return prefs;
   }
}
