/*  _____ _
 * |_   _| |_  _ _ ___ ___ _ __  __ _
 *   | | | ' \| '_/ -_) -_) '  \/ _` |_
 *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
 *
 * Threema for Android
 * Copyright (c) 2019-2020 Threema GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/*
 * Copyright (C) 2014 ZXing authors
 *
 * 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 ch.threema.app.qrscanner.camera;

import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.Camera;
import android.util.Log;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

/**
 * Utility methods for configuring the Android camera.
 *
 * @author Sean Owen
 */

/**
 * @date 2016-11-18 16:47
 * @auther GuoJinyu
 * @description modified
 */

final class CameraConfigurationUtils {

	private static final String TAG = "CameraConfiguration";

	private static final int MIN_PREVIEW_PIXELS = 480 * 320; // normal screen
	private static final float MAX_EXPOSURE_COMPENSATION = 1.5f;
	private static final float MIN_EXPOSURE_COMPENSATION = 0.0f;
	private static final double MAX_ASPECT_DISTORTION = 0.15;
	private static final int AREA_PER_1000 = 400;

	private CameraConfigurationUtils() {
	}

	static void setFocus(Camera.Parameters parameters,
	                     boolean autoFocus,
	                     boolean disableContinuous,
	                     boolean safeMode) {
		List<String> supportedFocusModes = parameters.getSupportedFocusModes();
		String focusMode = null;
		if (autoFocus) {
			if (safeMode || disableContinuous) {
				focusMode = findSettableValue("focus mode",
						supportedFocusModes,
						Camera.Parameters.FOCUS_MODE_AUTO);
			} else {
				focusMode = findSettableValue("focus mode",
						supportedFocusModes,
						Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
						Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
						Camera.Parameters.FOCUS_MODE_AUTO);
			}
		}
		// Maybe selected auto-focus but not available, so fall through here:
		if (!safeMode && focusMode == null) {
			focusMode = findSettableValue("focus mode",
					supportedFocusModes,
					Camera.Parameters.FOCUS_MODE_MACRO,
					Camera.Parameters.FOCUS_MODE_EDOF);
		}
		if (focusMode != null) {
			if (focusMode.equals(parameters.getFocusMode())) {
				//Log.i(TAG, "Focus mode already set to " + focusMode);
			} else {
				parameters.setFocusMode(focusMode);
			}
		}
	}

	static void setTorch(Camera.Parameters parameters, boolean on) {
		List<String> supportedFlashModes = parameters.getSupportedFlashModes();
		String flashMode;
		if (on) {
			flashMode = findSettableValue("flash mode",
					supportedFlashModes,
					Camera.Parameters.FLASH_MODE_TORCH,
					Camera.Parameters.FLASH_MODE_ON);
		} else {
			flashMode = findSettableValue("flash mode",
					supportedFlashModes,
					Camera.Parameters.FLASH_MODE_OFF);
		}
		if (flashMode != null) {
			if (flashMode.equals(parameters.getFlashMode())) {
				//Log.i(TAG, "Flash mode already set to " + flashMode);
			} else {
				//Log.i(TAG, "Setting flash mode to " + flashMode);
				parameters.setFlashMode(flashMode);
			}
		}
	}

	static void setBestExposure(Camera.Parameters parameters, boolean lightOn) {
		int minExposure = parameters.getMinExposureCompensation();
		int maxExposure = parameters.getMaxExposureCompensation();
		float step = parameters.getExposureCompensationStep();
		if ((minExposure != 0 || maxExposure != 0) && step > 0.0f) {
			// Set low when light is on
			float targetCompensation = lightOn ? MIN_EXPOSURE_COMPENSATION : MAX_EXPOSURE_COMPENSATION;
			int compensationSteps = Math.round(targetCompensation / step);
			float actualCompensation = step * compensationSteps;
			// Clamp value:
			compensationSteps = Math.max(Math.min(compensationSteps, maxExposure), minExposure);
			if (parameters.getExposureCompensation() == compensationSteps) {
				//Log.i(TAG, "Exposure compensation already set to " + compensationSteps + " / " + actualCompensation);
			} else {
				//Log.i(TAG, "Setting exposure compensation to " + compensationSteps + " / " + actualCompensation);
				parameters.setExposureCompensation(compensationSteps);
			}
		} else {
			//Log.i(TAG, "Camera does not support exposure compensation");
		}
	}


	static void setFocusArea(Camera.Parameters parameters) {
		if (parameters.getMaxNumFocusAreas() > 0) {
			//Log.i(TAG, "Old focus areas: " + toString(parameters.getFocusAreas()));
			List<Camera.Area> middleArea = buildMiddleArea(AREA_PER_1000);
			//Log.i(TAG, "Setting focus area to : " + toString(middleArea));
			parameters.setFocusAreas(middleArea);
		} else {
			//Log.i(TAG, "Device does not support focus areas");
		}
	}

	static void setMetering(Camera.Parameters parameters) {
		if (parameters.getMaxNumMeteringAreas() > 0) {
			//Log.i(TAG, "Old metering areas: " + parameters.getMeteringAreas());
			List<Camera.Area> middleArea = buildMiddleArea(AREA_PER_1000);
			//Log.i(TAG, "Setting metering area to : " + toString(middleArea));
			parameters.setMeteringAreas(middleArea);
		} else {
			//Log.i(TAG, "Device does not support metering areas");
		}
	}

	private static List<Camera.Area> buildMiddleArea(int areaPer1000) {
		return Collections.singletonList(
				new Camera.Area(new Rect(-areaPer1000, -areaPer1000, areaPer1000, areaPer1000), 1));
	}

	static void setVideoStabilization(Camera.Parameters parameters) {
		if (parameters.isVideoStabilizationSupported()) {
			if (parameters.getVideoStabilization()) {
				//Log.i(TAG, "Video stabilization already enabled");
			} else {
				//Log.i(TAG, "Enabling video stabilization...");
				parameters.setVideoStabilization(true);
			}
		} else {
			//Log.i(TAG, "This device does not support video stabilization");
		}
	}

	static void setBarcodeSceneMode(Camera.Parameters parameters) {
		if (Camera.Parameters.SCENE_MODE_BARCODE.equals(parameters.getSceneMode())) {
			//Log.i(TAG, "Barcode scene mode already set");
			return;
		}
		String sceneMode = findSettableValue("scene mode",
				parameters.getSupportedSceneModes(),
				Camera.Parameters.SCENE_MODE_BARCODE);
		if (sceneMode != null) {
			parameters.setSceneMode(sceneMode);
		}
	}

	static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {

		List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
		if (rawSupportedSizes == null) {
			//Log.w(TAG, "Device returned no supported preview sizes; using default");
			Camera.Size defaultSize = parameters.getPreviewSize();
			if (defaultSize == null) {
				throw new IllegalStateException("Parameters contained no preview size!");
			}
			return new Point(defaultSize.width, defaultSize.height);
		}

		// Sort by size, descending
		List<Camera.Size> supportedPreviewSizes = new ArrayList<>(rawSupportedSizes);
		Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() {
			@Override
			public int compare(Camera.Size a, Camera.Size b) {
				int aPixels = a.height * a.width;
				int bPixels = b.height * b.width;
				if (bPixels < aPixels) {
					return -1;
				}
				if (bPixels > aPixels) {
					return 1;
				}
				return 0;
			}
		});

		if (Log.isLoggable(TAG, Log.INFO)) {
			StringBuilder previewSizesString = new StringBuilder();
			for (Camera.Size supportedPreviewSize : supportedPreviewSizes) {
				previewSizesString.append(supportedPreviewSize.width).append('x')
						.append(supportedPreviewSize.height).append(' ');
			}
			//Log.i(TAG, "Supported preview sizes: " + previewSizesString);
		}

		double screenAspectRatio = screenResolution.x / (double) screenResolution.y;
		// Remove sizes that are unsuitable
		Iterator<Camera.Size> it = supportedPreviewSizes.iterator();
		while (it.hasNext()) {
			Camera.Size supportedPreviewSize = it.next();
			int realWidth = supportedPreviewSize.width;
			int realHeight = supportedPreviewSize.height;
			if (realWidth * realHeight < MIN_PREVIEW_PIXELS) {
				it.remove();
				continue;
			}
			boolean isScreenPortrait = screenResolution.x < screenResolution.y;
			int maybeFlippedWidth = isScreenPortrait ? realHeight : realWidth;
			int maybeFlippedHeight = isScreenPortrait ? realWidth : realHeight;
			double aspectRatio = (double) maybeFlippedWidth / (double) maybeFlippedHeight;
			double distortion = Math.abs(aspectRatio - screenAspectRatio);
			if (distortion > MAX_ASPECT_DISTORTION) {
				it.remove();
				continue;
			}

			if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
				Point exactPoint = new Point(realWidth, realHeight);
				//Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
				return exactPoint;
			}
		}

		// If no exact match, use largest preview size. This was not a great idea on older devices because
		// of the additional computation needed. We're likely to get here on newer Android 4+ devices, where
		// the CPU is much more powerful.
		if (!supportedPreviewSizes.isEmpty()) {
			Camera.Size largestPreview = supportedPreviewSizes.get(0);
			Point largestSize = new Point(largestPreview.width, largestPreview.height);
			//Log.i(TAG, "Using largest suitable preview size: " + largestSize);
			return largestSize;
		}

		// If there is nothing at all suitable, return current preview size
		Camera.Size defaultPreview = parameters.getPreviewSize();
		if (defaultPreview == null) {
			throw new IllegalStateException("Parameters contained no preview size!");
		}
		Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
		//Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
		return defaultSize;
	}

	private static String findSettableValue(String name,
	                                        Collection<String> supportedValues,
	                                        String... desiredValues) {
		//Log.i(TAG, "Requesting " + name + " value from among: " + Arrays.toString(desiredValues));
		//Log.i(TAG, "Supported " + name + " values: " + supportedValues);
		if (supportedValues != null) {
			for (String desiredValue : desiredValues) {
				if (supportedValues.contains(desiredValue)) {
					//Log.i(TAG, "Can set " + name + " to: " + desiredValue);
					return desiredValue;
				}
			}
		}
		//Log.i(TAG, "No supported values match");
		return null;
	}

	private static String toString(Iterable<Camera.Area> areas) {
		if (areas == null) {
			return null;
		}
		StringBuilder result = new StringBuilder();
		for (Camera.Area area : areas) {
			result.append(area.rect).append(':').append(area.weight).append(' ');
		}
		return result.toString();
	}

}
