/*
 * Copyright 2012-2015 the original author or authors.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

package eu.siacs.conversations.ui.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import com.google.zxing.PlanarYUVLuminanceSource;

import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.PreviewCallback;
import android.util.Log;
import android.view.TextureView;

import eu.siacs.conversations.Config;

/**
 * @author Andreas Schildbach
 */
@SuppressWarnings("deprecation")
public final class CameraManager {
    private static final int MIN_FRAME_SIZE = 240;
    private static final int MAX_FRAME_SIZE = 600;
    private static final int MIN_PREVIEW_PIXELS = 470 * 320; // normal screen
    private static final int MAX_PREVIEW_PIXELS = 1280 * 720;

    private Camera camera;
    private CameraInfo cameraInfo = new CameraInfo();
    private Camera.Size cameraResolution;
    private Rect frame;
    private RectF framePreview;

    public Rect getFrame() {
        return frame;
    }

    public RectF getFramePreview() {
        return framePreview;
    }

    public int getFacing() {
        return cameraInfo.facing;
    }

    public int getOrientation() {
        return cameraInfo.orientation;
    }

    public Camera open(final TextureView textureView, final int displayOrientation, final boolean continuousAutoFocus)
            throws IOException {
        final int cameraId = determineCameraId();
        Camera.getCameraInfo(cameraId, cameraInfo);

        camera = Camera.open(cameraId);

        if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
            camera.setDisplayOrientation((720 - displayOrientation - cameraInfo.orientation) % 360);
        else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK)
            camera.setDisplayOrientation((720 - displayOrientation + cameraInfo.orientation) % 360);
        else
            throw new IllegalStateException("facing: " + cameraInfo.facing);

        camera.setPreviewTexture(textureView.getSurfaceTexture());

        final int width = textureView.getWidth();
        final int height = textureView.getHeight();

        final Camera.Parameters parameters = camera.getParameters();

        cameraResolution = findBestPreviewSizeValue(parameters, width, height);

        final int rawSize = Math.min(width * 2 / 3, height * 2 / 3);
        final int frameSize = Math.max(MIN_FRAME_SIZE, Math.min(MAX_FRAME_SIZE, rawSize));

        final int leftOffset = (width - frameSize) / 2;
        final int topOffset = (height - frameSize) / 2;
        frame = new Rect(leftOffset, topOffset, leftOffset + frameSize, topOffset + frameSize);

        float widthFactor;
        float heightFactor;
        Rect orientedFrame;
        boolean isTexturePortrait = width < height;
        boolean isCameraPortrait = cameraResolution.width < cameraResolution.height;
        if (isTexturePortrait == isCameraPortrait) {
            widthFactor = (float)cameraResolution.width / width;
            heightFactor = (float)cameraResolution.height / height;
            orientedFrame = new Rect(frame);
        } else {
            widthFactor = (float)cameraResolution.width / height;
            heightFactor = (float)cameraResolution.height / width;
            // Swap X and Y coordinates to flip frame to the same orientation as cameraResolution
            orientedFrame = new Rect(frame.top, frame.left, frame.bottom, frame.right);
        }

        framePreview = new RectF(orientedFrame.left * widthFactor, orientedFrame.top * heightFactor,
                orientedFrame.right * widthFactor, orientedFrame.bottom * heightFactor);

        final String savedParameters = parameters == null ? null : parameters.flatten();

        try {
            setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
        } catch (final RuntimeException x) {
            if (savedParameters != null) {
                final Camera.Parameters parameters2 = camera.getParameters();
                parameters2.unflatten(savedParameters);
                try {
                    camera.setParameters(parameters2);
                    setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
                } catch (final RuntimeException x2) {
                    Log.d(Config.LOGTAG,"problem setting camera parameters", x2);
                }
            }
        }

        try {
            camera.startPreview();
            return camera;
        } catch (final RuntimeException x) {
            Log.w(Config.LOGTAG,"something went wrong while starting camera preview", x);
            camera.release();
            throw x;
        }
    }

    private int determineCameraId() {
        final int cameraCount = Camera.getNumberOfCameras();
        final CameraInfo cameraInfo = new CameraInfo();

        // prefer back-facing camera
        for (int i = 0; i < cameraCount; i++) {
            Camera.getCameraInfo(i, cameraInfo);
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK)
                return i;
        }

        // fall back to front-facing camera
        for (int i = 0; i < cameraCount; i++) {
            Camera.getCameraInfo(i, cameraInfo);
            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
                return i;
        }

        return -1;
    }

    public void close() {
        if (camera != null) {
            try {
                camera.stopPreview();
            } catch (final RuntimeException x) {
                Log.w(Config.LOGTAG,"something went wrong while stopping camera preview", x);
            }

            camera.release();
        }
    }

    private static final Comparator<Camera.Size> numPixelComparator = new Comparator<Camera.Size>() {
        @Override
        public int compare(final Camera.Size size1, final Camera.Size size2) {
            final int pixels1 = size1.height * size1.width;
            final int pixels2 = size2.height * size2.width;

            if (pixels1 < pixels2)
                return 1;
            else if (pixels1 > pixels2)
                return -1;
            else
                return 0;
        }
    };

    private static Camera.Size findBestPreviewSizeValue(final Camera.Parameters parameters, int width, int height) {
        if (height > width) {
            final int temp = width;
            width = height;
            height = temp;
        }

        final float screenAspectRatio = (float) width / (float) height;

        final List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
        if (rawSupportedSizes == null)
            return parameters.getPreviewSize();

        // sort by size, descending
        final List<Camera.Size> supportedPreviewSizes = new ArrayList<Camera.Size>(rawSupportedSizes);
        Collections.sort(supportedPreviewSizes, numPixelComparator);

        Camera.Size bestSize = null;
        float diff = Float.POSITIVE_INFINITY;

        for (final Camera.Size supportedPreviewSize : supportedPreviewSizes) {
            final int realWidth = supportedPreviewSize.width;
            final int realHeight = supportedPreviewSize.height;
            final int realPixels = realWidth * realHeight;
            if (realPixels < MIN_PREVIEW_PIXELS || realPixels > MAX_PREVIEW_PIXELS)
                continue;

            final boolean isCandidatePortrait = realWidth < realHeight;
            final int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
            final int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
            if (maybeFlippedWidth == width && maybeFlippedHeight == height)
                return supportedPreviewSize;

            final float aspectRatio = (float) maybeFlippedWidth / (float) maybeFlippedHeight;
            final float newDiff = Math.abs(aspectRatio - screenAspectRatio);
            if (newDiff < diff) {
                bestSize = supportedPreviewSize;
                diff = newDiff;
            }
        }

        if (bestSize != null)
            return bestSize;
        else
            return parameters.getPreviewSize();
    }

    @SuppressLint("InlinedApi")
    private static void setDesiredCameraParameters(final Camera camera, final Camera.Size cameraResolution,
            final boolean continuousAutoFocus) {
        final Camera.Parameters parameters = camera.getParameters();
        if (parameters == null)
            return;

        final List<String> supportedFocusModes = parameters.getSupportedFocusModes();
        final String focusMode = continuousAutoFocus
                ? findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
                        Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, Camera.Parameters.FOCUS_MODE_AUTO,
                        Camera.Parameters.FOCUS_MODE_MACRO)
                : findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_AUTO, Camera.Parameters.FOCUS_MODE_MACRO);
        if (focusMode != null)
            parameters.setFocusMode(focusMode);

        parameters.setPreviewSize(cameraResolution.width, cameraResolution.height);

        camera.setParameters(parameters);
    }

    public void requestPreviewFrame(final PreviewCallback callback) {
        try {
            camera.setOneShotPreviewCallback(callback);
        } catch (final RuntimeException x) {
            Log.d(Config.LOGTAG,"problem requesting preview frame, callback won't be called", x);
        }
    }

    public PlanarYUVLuminanceSource buildLuminanceSource(final byte[] data) {
        return new PlanarYUVLuminanceSource(data, cameraResolution.width, cameraResolution.height,
                (int) framePreview.left, (int) framePreview.top, (int) framePreview.width(),
                (int) framePreview.height(), false);
    }

    public void setTorch(final boolean enabled) {
        if (enabled != getTorchEnabled(camera))
            setTorchEnabled(camera, enabled);
    }

    private static boolean getTorchEnabled(final Camera camera) {
        final Camera.Parameters parameters = camera.getParameters();
        if (parameters != null) {
            final String flashMode = camera.getParameters().getFlashMode();
            return flashMode != null && (Camera.Parameters.FLASH_MODE_ON.equals(flashMode)
                    || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode));
        }

        return false;
    }

    private static void setTorchEnabled(final Camera camera, final boolean enabled) {
        final Camera.Parameters parameters = camera.getParameters();

        final List<String> supportedFlashModes = parameters.getSupportedFlashModes();
        if (supportedFlashModes != null) {
            final String flashMode;
            if (enabled)
                flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_TORCH,
                        Camera.Parameters.FLASH_MODE_ON);
            else
                flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF);

            if (flashMode != null) {
                camera.cancelAutoFocus(); // autofocus can cause conflict

                parameters.setFlashMode(flashMode);
                camera.setParameters(parameters);
            }
        }
    }

    private static String findValue(final Collection<String> values, final String... valuesToFind) {
        for (final String valueToFind : valuesToFind)
            if (values.contains(valueToFind))
                return valueToFind;

        return null;
    }
}
