/*
 * Copyright (C) 2019 Electric Coin Company
 *
 * Copyright (C) 2016 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
 *
 * This file has been modified by Electric Coin Company to translate it into Kotlin and add support for Firebase vision.
 *
 * 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 cash.z.android.cameraview

import android.app.Activity
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.Image
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.Surface
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.IntDef
import androidx.annotation.NonNull
import androidx.annotation.Nullable
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import cash.z.android.cameraview.api21.Camera2
import cash.z.android.cameraview.base.AspectRatio
import cash.z.android.cameraview.base.CameraViewImpl
import cash.z.android.cameraview.base.Constants
import cash.z.android.cameraview.base.PreviewImpl
import cash.z.android.qrecycler.R
import com.google.android.cameraview.Camera2Api23
import com.google.android.cameraview.TextureViewPreview
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
import java.util.*

open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
    FrameLayout(context, attrs, defStyleAttr) {

    @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs, 0)

    internal lateinit var  mImpl: CameraViewImpl

    private var mCallbacks: CallbackBridge?

    private var mAdjustViewBounds: Boolean = false

    private var mDisplayOrientationDetector: DisplayOrientationDetector?

    var firebaseCallback: FirebaseCallback? = null
        set(value) {
            (mImpl as? Camera2)?.firebaseCallback = value
            field = value
        }


    lateinit var cameraId: String


    /**
     * @return `true` if the camera is opened.
     */
    val isCameraOpened: Boolean
        get() = mImpl.isCameraOpened

    /**
     * @return True when this CameraView is adjusting its bounds to preserve the aspect ratio of
     * camera.
     * @see .setAdjustViewBounds
     */
    /**
     * @param adjustViewBounds `true` if you want the CameraView to adjust its bounds to
     * preserve the aspect ratio of camera.
     * @see .getAdjustViewBounds
     */
    var adjustViewBounds: Boolean
        get() = mAdjustViewBounds
        set(adjustViewBounds) {
            if (mAdjustViewBounds != adjustViewBounds) {
                mAdjustViewBounds = adjustViewBounds
                requestLayout()
            }
        }

    /**
     * Gets the direction that the current camera faces.
     *
     * @return The camera facing.
     */
    /**
     * Chooses camera by the direction it faces.
     *
     * @param facing The camera facing. Must be either [.FACING_BACK] or
     * [.FACING_FRONT].
     */
    var facing: Int
        @Facing
        get() = mImpl.facing
        set(@Facing facing) {
            mImpl.facing = facing
        }

    /**
     * Gets all the aspect ratios supported by the current camera.
     */
    val supportedAspectRatios: Set<AspectRatio>
        get() = mImpl.supportedAspectRatios

    /**
     * Gets the current aspect ratio of camera.
     *
     * @return The current [AspectRatio]. Can be `null` if no camera is opened yet.
     */
    /**
     * Sets the aspect ratio of camera.
     *
     * @param ratio The [AspectRatio] to be set.
     */
    var aspectRatio: AspectRatio?
        @Nullable
        get() = mImpl.aspectRatio
        set(@NonNull ratio) {
            if (mImpl.setAspectRatio(ratio)) {
                requestLayout()
            }
        }

    /**
     * Returns whether the continuous auto-focus mode is enabled.
     *
     * @return `true` if the continuous auto-focus mode is enabled. `false` if it is
     * disabled, or if it is not supported by the current camera.
     */
    /**
     * Enables or disables the continuous auto-focus mode. When the current camera doesn't support
     * auto-focus, calling this method will be ignored.
     *
     * @param autoFocus `true` to enable continuous auto-focus mode. `false` to
     * disable it.
     */
    var autoFocus: Boolean
        get() = mImpl.autoFocus
        set(autoFocus) {
            mImpl.autoFocus = autoFocus
        }

    /**
     * Gets the current flash mode.
     *
     * @return The current flash mode.
     */
    /**
     * Sets the flash mode.
     *
     * @param flash The desired flash mode.
     */
    var flash: Int
        @Flash
        get() = mImpl.flash
        set(@Flash flash) {
            mImpl.flash = flash
        }

    /** Direction the camera faces relative to device screen.  */
    @IntDef(FACING_BACK, FACING_FRONT)
    @kotlin.annotation.Retention(AnnotationRetention.SOURCE)
    annotation class Facing

    /** The mode for for the camera device's flash control  */
    @IntDef(FLASH_OFF, FLASH_ON, FLASH_TORCH, FLASH_AUTO, FLASH_RED_EYE)
    annotation class Flash

    init {
        if (isInEditMode) {
            mCallbacks = null
            mDisplayOrientationDetector = null
        } else {
            // Internal setup
            val preview = createPreviewImpl(context)
            mCallbacks = CallbackBridge()
            if (Build.VERSION.SDK_INT < 23) {
                mImpl = Camera2(mCallbacks!!, preview, context)
            } else {
                mImpl = Camera2Api23(mCallbacks!!, preview, context)
            }
            // Attributes
            val a = context.obtainStyledAttributes(
                attrs, R.styleable.CameraView, defStyleAttr,
                R.style.Widget_CameraView
            )
            mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false)
            facing = a.getInt(R.styleable.CameraView_facing, FACING_BACK)
            var aspectRatioString = a.getString(R.styleable.CameraView_aspectRatio)
            if (aspectRatioString != null) {
                aspectRatio = AspectRatio.parse(aspectRatioString)
            } else {
                aspectRatio = Constants.DEFAULT_ASPECT_RATIO
            }
            autoFocus = a.getBoolean(R.styleable.CameraView_autoFocus, true)
            flash = a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO)
            a.recycle()
            // Display orientation detector
            mDisplayOrientationDetector = object : DisplayOrientationDetector(context) {
                override fun onDisplayOrientationChanged(displayOrientation: Int) {
                    mImpl.setDisplayOrientation(displayOrientation)
                }
            }
        }
    }

    @NonNull
    private fun createPreviewImpl(context: Context): PreviewImpl {
        return TextureViewPreview(context, this)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        if (!isInEditMode) {
            mDisplayOrientationDetector!!.enable(ViewCompat.getDisplay(this)!!)
        }
    }

    override fun onDetachedFromWindow() {
        if (!isInEditMode) {
            mDisplayOrientationDetector!!.disable()
        }
        super.onDetachedFromWindow()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        if (isInEditMode) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            return
        }
        // Handle android:adjustViewBounds
        if (mAdjustViewBounds) {
            if (!isCameraOpened) {
                mCallbacks!!.reserveRequestLayoutOnOpen()
                super.onMeasure(widthMeasureSpec, heightMeasureSpec)
                return
            }
            val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
            val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
            if (widthMode == View.MeasureSpec.EXACTLY && heightMode != View.MeasureSpec.EXACTLY) {
                val ratio = aspectRatio!!
                var height = (View.MeasureSpec.getSize(widthMeasureSpec) * ratio!!.toFloat()) as Int
                if (heightMode == View.MeasureSpec.AT_MOST) {
                    height = Math.min(height, View.MeasureSpec.getSize(heightMeasureSpec))
                }
                super.onMeasure(
                    widthMeasureSpec,
                    View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
                )
            } else if (widthMode != View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY) {
                val ratio = aspectRatio!!
                var width = (View.MeasureSpec.getSize(heightMeasureSpec) * ratio!!.toFloat()) as Int
                if (widthMode == View.MeasureSpec.AT_MOST) {
                    width = Math.min(width, View.MeasureSpec.getSize(widthMeasureSpec))
                }
                super.onMeasure(
                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
                    heightMeasureSpec
                )
            } else {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            }
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }
        // Measure the TextureView
        val width = measuredWidth
        val height = measuredHeight
        var ratio = aspectRatio
        if (mDisplayOrientationDetector!!.lastKnownDisplayOrientation % 180 == 0) {
            ratio = ratio!!.inverse()
        }
        assert(ratio != null)
        if (height < width * ratio!!.y / ratio!!.x) {
            mImpl.view.measure(
                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
                View.MeasureSpec.makeMeasureSpec(
                    width * ratio!!.y / ratio!!.x,
                    View.MeasureSpec.EXACTLY
                )
            )
        } else {
            mImpl.view.measure(
                View.MeasureSpec.makeMeasureSpec(
                    height * ratio!!.x / ratio!!.y,
                    View.MeasureSpec.EXACTLY
                ),
                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
            )
        }
    }

    private var rectPaint = Paint().apply {
        color = Color.GREEN
        style = Paint.Style.FILL
        strokeWidth = 8f
    }

    override fun draw(canvas: Canvas) {
        super.draw(canvas)
        val rect = RectF(0f,0f,canvas.width.toFloat(),canvas.height.toFloat())
        canvas.drawRect(rect, rectPaint)
    }

    override fun onSaveInstanceState(): Parcelable? {
        val state = SavedState(super.onSaveInstanceState())
        state.facing = facing
        state.ratio = aspectRatio
        state.autoFocus = autoFocus
        state.flash = flash
        return state
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        if (state !is SavedState) {
            super.onRestoreInstanceState(state)
            return
        }
        val ss = state as SavedState?
        super.onRestoreInstanceState(ss!!.getSuperState())
        facing = ss.facing
        aspectRatio = ss.ratio
        autoFocus = ss.autoFocus
        flash = ss.flash
    }

    /**
     * Open a camera device and start showing camera preview. This is typically called from
     * [Activity.onResume].
     */
    fun start() {
        if (!mImpl.start()) {
            throw IllegalStateException("failed to start even though we're on API 21+")
//            //store the state ,and restore this state after fall back o Camera1
//            val state = onSaveInstanceState()
//            // Camera2 uses legacy hardware layer; fall back to Camera1
//            mImpl = Camera1(mCallbacks, createPreviewImpl(context))
//            onRestoreInstanceState(state)
//            mImpl.start()
        }

        // start results in cameraId being set so bubble that up for firebase rotation use
        when(mImpl) {
            is Camera2 -> cameraId = (mImpl as Camera2).cameraId!!
        }
    }

    /**
     * Stop camera preview and close the device. This is typically called from
     * [Activity.onPause].
     */
    fun stop() {
        mImpl.stop()
    }

    /**
     * Add a new callback.
     *
     * @param callback The [Callback] to add.
     * @see .removeCallback
     */
    fun addCallback(@NonNull callback: Callback) {
        mCallbacks!!.add(callback)
    }

    /**
     * Remove a callback.
     *
     * @param callback The [Callback] to remove.
     * @see .addCallback
     */
    fun removeCallback(@NonNull callback: Callback) {
        mCallbacks!!.remove(callback)
    }

    /**
     * Take a picture. The result will be returned to
     * [Callback.onPictureTaken].
     */
    fun takePicture() {
        mImpl.takePicture()
    }

    fun setBarcode(barcode: FirebaseVisionBarcode) {

    }

    interface FirebaseCallback {
        fun onImageAvailable(image: Image)

        // TODO: attribute this code. The library I found it in has no attribution but it clearly came from somewhere. Modified it to not require instantiating a sparsearray of orientations (just use when instead) also simplified method signature
        // one source : https://github.com/firebase/snippets-android/blob/master/mlkit/app/src/main/java/com/google/firebase/example/mlkit/VisionImage.java
        /**
         * Get the angle by which an image must be rotated given the device's current
         * orientation.
         */
        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
        @Throws(CameraAccessException::class)
        fun getRotationCompensation(cameraId: String, activity: Activity): Int {
            // Get the device's current rotation relative to its "native" orientation.
            // Then, from the ORIENTATIONS table, look up the angle the image must be
            // rotated to compensate for the device's rotation.
            val deviceRotation = activity.windowManager.defaultDisplay.rotation
            var rotationCompensation = when(deviceRotation) {
                Surface.ROTATION_0 -> 90
                Surface.ROTATION_90 -> 0
                Surface.ROTATION_180 -> 270
                Surface.ROTATION_270 -> 180
                else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation")
            }

            // On most devices, the sensor orientation is 90 degrees, but for some
            // devices it is 270 degrees. For devices with a sensor orientation of
            // 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
            val cameraManager = activity.getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
            val sensorOrientation = cameraManager
                .getCameraCharacteristics(cameraId)
                .get(CameraCharacteristics.SENSOR_ORIENTATION)!!
            rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360

            // Return the corresponding FirebaseVisionImageMetadata rotation value.
            val result: Int
            when (rotationCompensation) {
                0 -> result = FirebaseVisionImageMetadata.ROTATION_0
                90 -> result = FirebaseVisionImageMetadata.ROTATION_90
                180 -> result = FirebaseVisionImageMetadata.ROTATION_180
                270 -> result = FirebaseVisionImageMetadata.ROTATION_270
                else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation") // this should be impossible, given that we would have already thrown an exception
            }
            return result
        }
    }

    private inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {

        private val mCallbacks = ArrayList<Callback>()

        private var mRequestLayoutOnOpen: Boolean = false

        fun add(callback: Callback) {
            mCallbacks.add(callback)
        }

        fun remove(callback: Callback) {
            mCallbacks.remove(callback)
        }

        override fun onCameraOpened() {
            if (mRequestLayoutOnOpen) {
                mRequestLayoutOnOpen = false
                requestLayout()
            }
            for (callback in mCallbacks) {
                callback.onCameraOpened(this@CameraView)
            }
        }

        override fun onCameraClosed() {
            for (callback in mCallbacks) {
                callback.onCameraClosed(this@CameraView)
            }
        }

        override fun onPictureTaken(data: ByteArray) {
            for (callback in mCallbacks) {
                callback.onPictureTaken(this@CameraView, data)
            }
        }

        fun reserveRequestLayoutOnOpen() {
            mRequestLayoutOnOpen = true
        }
    }

    protected class SavedState : View.BaseSavedState {

        @Facing
        internal var facing: Int = 0

        internal var ratio: AspectRatio? = null

        internal var autoFocus: Boolean = false

        @Flash
        internal var flash: Int = 0

        constructor(source: Parcel) : this(source, AspectRatio::class.java.classLoader!!)

        constructor(source: Parcel, loader: ClassLoader) : super(source) {
            facing = source.readInt()
            ratio = source.readParcelable(loader)
            autoFocus = source.readByte().toInt() != 0
            flash = source.readInt()
        }

        constructor(parcelable: Parcelable) : super(parcelable)

        override fun writeToParcel(out: Parcel, flags: Int) {
            super.writeToParcel(out, flags)
            out.writeInt(facing)
            out.writeParcelable(ratio, 0)
            out.writeByte((if (autoFocus) 1 else 0).toByte())
            out.writeInt(flash)
        }

        override fun describeContents(): Int {
            return 0
        }

        companion object CREATOR : Parcelable.Creator<SavedState> {
            override fun createFromParcel(parcel: Parcel): SavedState {
                return SavedState(parcel)
            }

            override fun newArray(size: Int): Array<SavedState?> {
                return arrayOfNulls(size)
            }
        }

    }

    /**
     * Callback for monitoring events about [CameraView].
     */
    abstract class Callback {

        /**
         * Called when camera is opened.
         *
         * @param cameraView The associated [CameraView].
         */
        open fun onCameraOpened(cameraView: CameraView) {}

        /**
         * Called when camera is closed.
         *
         * @param cameraView The associated [CameraView].
         */
        open fun onCameraClosed(cameraView: CameraView) {}

        /**
         * Called when a picture is taken.
         *
         * @param cameraView The associated [CameraView].
         * @param data       JPEG data.
         */
        open fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
    }

    companion object {

        /** The camera device faces the opposite direction as the device's screen.  */
        const val FACING_BACK = Constants.FACING_BACK

        /** The camera device faces the same direction as the device's screen.  */
        const val FACING_FRONT = Constants.FACING_FRONT

        /** Flash will not be fired.  */
        const val FLASH_OFF = Constants.FLASH_OFF

        /** Flash will always be fired during snapshot.  */
        const val FLASH_ON = Constants.FLASH_ON

        /** Constant emission of light during preview, auto-focus and snapshot.  */
        const val FLASH_TORCH = Constants.FLASH_TORCH

        /** Flash will be fired automatically when required.  */
        const val FLASH_AUTO = Constants.FLASH_AUTO

        /** Flash will be fired in red-eye reduction mode.  */
        const val FLASH_RED_EYE = Constants.FLASH_RED_EYE
    }

}
