# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_ov5640`
================================================================================

CircuitPython driver for OV5640 Camera


* Author(s): Jeff Epler

Implementation Notes
--------------------

**Hardware:**

* ESP32-S2 Kaluga Dev Kit featuring ESP32-S2 WROVER <https://www.adafruit.com/product/4729>

**Software and Dependencies:**

* Adafruit CircuitPython firmware for the supported boards:
  https://github.com/adafruit/circuitpython/releases
"""

# pylint: disable=too-many-lines

# imports
import time
import imagecapture
import pwmio
from adafruit_bus_device.i2c_device import I2CDevice

try:
    from typing import Optional, Sequence, List, Union
    from busio import I2C
    from microcontroller import Pin
    from digitalio import DigitalInOut
except ImportError:
    pass

__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_ov5640.git"

from micropython import const

OV5640_COLOR_RGB = 0
OV5640_COLOR_YUV = 1
OV5640_COLOR_GRAYSCALE = 2
OV5640_COLOR_JPEG = 3

# fmt: off

_SYSTEM_RESET00 = const(0x3000) # Reset for Individual Block
# (0: enable block; 1: reset block)
# Bit[7]: Reset BIST
# Bit[6]: Reset MCU program memory
# Bit[5]: Reset MCU
# Bit[4]: Reset OTP
# Bit[3]: Reset STB
# Bit[2]: Reset d5060
# Bit[1]: Reset timing control
# Bit[0]: Reset array control

_SYSTEM_RESET02 = const(0x3002) # Reset for Individual Block
# (0: enable block; 1: reset block)
# Bit[7]: Reset VFIFO
# Bit[5]: Reset format
# Bit[4]: Reset JFIFO
# Bit[3]: Reset SFIFO
# Bit[2]: Reset JPG
# Bit[1]: Reset format MUX
# Bit[0]: Reset average

_CLOCK_ENABLE02 = const(0x3006) # Clock Enable Control
# (0: disable clock; 1: enable clock)
# Bit[7]: Enable PSRAM clock
# Bit[6]: Enable FMT clock
# Bit[5]: Enable JPEG 2x clock
# Bit[3]: Enable JPEG clock
# Bit[1]: Enable format MUX clock
# Bit[0]: Enable average clock

_SYSTEM_CTROL0 = const(0x3008)
# Bit[7]: Software reset
# Bit[6]: Software power down
# Bit[5]: Reserved
# Bit[4]: SRB clock SYNC enable
# Bit[3]: Isolation suspend select
# Bit[2:0]: Not used

_CHIP_ID_HIGH = const(0x300A)

_DRIVE_CAPABILITY = const(0x302C)
# Bit[7:6]:
#          00: 1x
#          01: 2x
#          10: 3x
#          11: 4x

_SC_PLLS_CTRL0 = const(0x303A)
# Bit[7]: PLLS bypass
_SC_PLLS_CTRL1 = const(0x303B)
# Bit[4:0]: PLLS multiplier
_SC_PLLS_CTRL2 = const(0x303C)
# Bit[6:4]: PLLS charge pump control
# Bit[3:0]: PLLS system divider
_SC_PLLS_CTRL3 = const(0x303D)
# Bit[5:4]: PLLS pre-divider
#          00: 1
#          01: 1.5
#          10: 2
#          11: 3
# Bit[2]: PLLS root-divider - 1
# Bit[1:0]: PLLS seld5
#          00: 1
#          01: 1
#          10: 2
#          11: 2.5

# AEC/AGC control functions
_AEC_PK_MANUAL = const(0x3503)
# AEC Manual Mode Control
# Bit[7:6]: Reserved
# Bit[5]: Gain delay option
#         Valid when 0x3503[4]=1’b0
#         0: Delay one frame latch
#         1: One frame latch
# Bit[4:2]: Reserved
# Bit[1]: AGC manual
#         0: Auto enable
#         1: Manual enable
# Bit[0]: AEC manual
#         0: Auto enable
#         1: Manual enable

# gain = {0x350A[1:0], 0x350B[7:0]} / 16


_X_ADDR_ST_H = const(0x3800)
# Bit[3:0]: X address start[11:8]
_X_ADDR_ST_L = const(0x3801)
# Bit[7:0]: X address start[7:0]
_Y_ADDR_ST_H = const(0x3802)
# Bit[2:0]: Y address start[10:8]
_Y_ADDR_ST_L = const(0x3803)
# Bit[7:0]: Y address start[7:0]
_X_ADDR_END_H = const(0x3804)
# Bit[3:0]: X address end[11:8]
_X_ADDR_END_L = const(0x3805)
# Bit[7:0]:
_Y_ADDR_END_H = const(0x3806)
# Bit[2:0]: Y address end[10:8]
_Y_ADDR_END_L = const(0x3807)
# Bit[7:0]:
# Size after scaling
_X_OUTPUT_SIZE_H = const(0x3808)
# Bit[3:0]: DVP output horizontal width[11:8]
_X_OUTPUT_SIZE_L = const(0x3809)
# Bit[7:0]:
_Y_OUTPUT_SIZE_H = const(0x380A)
# Bit[2:0]: DVP output vertical height[10:8]
_Y_OUTPUT_SIZE_L = const(0x380B)
# Bit[7:0]:
_X_TOTAL_SIZE_H = const(0x380C)
# Bit[3:0]: Total horizontal size[11:8]
_X_TOTAL_SIZE_L = const(0x380D)
# Bit[7:0]:
_Y_TOTAL_SIZE_H = const(0x380E)
# Bit[7:0]: Total vertical size[15:8]
_Y_TOTAL_SIZE_L = const(0x380F)
# Bit[7:0]:
_X_OFFSET_H = const(0x3810)
# Bit[3:0]: ISP horizontal offset[11:8]
_X_OFFSET_L = const(0x3811)
# Bit[7:0]:
_Y_OFFSET_H = const(0x3812)
# Bit[2:0]: ISP vertical offset[10:8]
_Y_OFFSET_L = const(0x3813)
# Bit[7:0]:
_X_INCREMENT = const(0x3814)
# Bit[7:4]: Horizontal odd subsample increment
# Bit[3:0]: Horizontal even subsample increment
_Y_INCREMENT = const(0x3815)
# Bit[7:4]: Vertical odd subsample increment
# Bit[3:0]: Vertical even subsample increment
# Size before scaling
# X_INPUT_SIZE = const(   (X_ADDR_END - X_ADDR_ST + 1 - (2 * X_OFFSET)))
# Y_INPUT_SIZE = const(   (Y_ADDR_END - Y_ADDR_ST + 1 - (2 * Y_OFFSET)))

# mirror and flip registers
_TIMING_TC_REG20 = const(0x3820)
# Timing Control Register
# Bit[2:1]: Vertical flip enable
#         00: Normal
#         11: Vertical flip
# Bit[0]: Vertical binning enable
_TIMING_TC_REG21 = const(0x3821)
# Timing Control Register
# Bit[5]: Compression Enable
# Bit[2:1]: Horizontal mirror enable
#         00: Normal
#         11: Horizontal mirror
# Bit[0]: Horizontal binning enable

_PCLK_RATIO = const(0x3824)
# Bit[4:0]: PCLK ratio manual

# frame control registers
_FRAME_CTRL01 = const(
    0x4201
)
# Control Passed Frame Number When both ON and OFF number set to 0x00,frame
# control is in bypass mode
# Bit[7:4]: Not used
# Bit[3:0]: Frame ON number
_FRAME_CTRL02 = const(
    0x4202
)
# Control Masked Frame Number When both ON and OFF number set to 0x00,frame
# control is in bypass mode
# Bit[7:4]: Not used
# BIT[3:0]: Frame OFF number

# format control registers
_FORMAT_CTRL00 = const(0x4300)

_CLOCK_POL_CONTROL = const(0x4740)
# Bit[5]: PCLK polarity 0: active low
#          1: active high
# Bit[3]: Gate PCLK under VSYNC
# Bit[2]: Gate PCLK under HREF
# Bit[1]: HREF polarity
#          0: active low
#          1: active high
# Bit[0] VSYNC polarity
#          0: active low
#          1: active high

_ISP_CONTROL_01 = const(0x5001)
# Bit[5]: Scale enable
#          0: Disable
#          1: Enable

# output format control registers
_FORMAT_CTRL = const(0x501F)
# Format select
# Bit[2:0]:
#  000: YUV422
#  001: RGB
#  010: Dither
#  011: RAW after DPC
#  101: RAW after CIP

# ISP top control registers
_PRE_ISP_TEST_SETTING_1 = const(0x503D)
# Bit[7]: Test enable
#         0: Test disable
#         1: Color bar enable
# Bit[6]: Rolling
# Bit[5]: Transparent
# Bit[4]: Square black and white
# Bit[3:2]: Color bar style
#         00: Standard 8 color bar
#         01: Gradual change at vertical mode 1
#         10: Gradual change at horizontal
#         11: Gradual change at vertical mode 2
# Bit[1:0]: Test select
#         00: Color bar
#         01: Random data
#         10: Square data
#         11: Black image

# exposure = {0x3500[3:0], 0x3501[7:0], 0x3502[7:0]} / 16 × tROW

_SCALE_CTRL_1 = const(0x5601)
# Bit[6:4]: HDIV RW
#          DCW scale times
#          000: DCW 1 time
#          001: DCW 2 times
#          010: DCW 4 times
#          100: DCW 8 times
#          101: DCW 16 times
#          Others: DCW 16 times
# Bit[2:0]: VDIV RW
#          DCW scale times
#          000: DCW 1 time
#          001: DCW 2 times
#          010: DCW 4 times
#          100: DCW 8 times
#          101: DCW 16 times
#          Others: DCW 16 times

_SCALE_CTRL_2 = const(0x5602)
# X_SCALE High Bits
_SCALE_CTRL_3 = const(0x5603)
# X_SCALE Low Bits
_SCALE_CTRL_4 = const(0x5604)
# Y_SCALE High Bits
_SCALE_CTRL_5 = const(0x5605)
# Y_SCALE Low Bits
_SCALE_CTRL_6 = const(0x5606)
# Bit[3:0]: V Offset

_VFIFO_CTRL0C = const(0x460C)
# Bit[1]: PCLK manual enable
#          0: Auto
#          1: Manual by PCLK_RATIO

_VFIFO_X_SIZE_H = const(0x4602)
_VFIFO_X_SIZE_L = const(0x4603)
_VFIFO_Y_SIZE_H = const(0x4604)
_VFIFO_Y_SIZE_L = const(0x4605)

_COMPRESSION_CTRL00 = const(0x4400)
_COMPRESSION_CTRL01 = const(0x4401)
_COMPRESSION_CTRL02 = const(0x4402)
_COMPRESSION_CTRL03 = const(0x4403)
_COMPRESSION_CTRL04 = const(0x4404)
_COMPRESSION_CTRL05 = const(0x4405)
_COMPRESSION_CTRL06 = const(0x4406)
_COMPRESSION_CTRL07 = const(0x4407)
# Bit[5:0]: QS
_COMPRESSION_ISI_CTRL = const(0x4408)
_COMPRESSION_CTRL09 = const(0x4409)
_COMPRESSION_CTRL0A = const(0x440A)
_COMPRESSION_CTRL0B = const(0x440B)
_COMPRESSION_CTRL0C = const(0x440C)
_COMPRESSION_CTRL0D = const(0x440D)
_COMPRESSION_CTRL0E = const(0x440E)

_TEST_COLOR_BAR = const(0xC0)
# Enable Color Bar roling Test

_AEC_PK_MANUAL_AGC_MANUALEN = const(0x02)
# Enable AGC Manual enable
_AEC_PK_MANUAL_AEC_MANUALEN = const(0x01)
# Enable AEC Manual enable

_TIMING_TC_REG20_VFLIP = const(0x06)
# Vertical flip enable
_TIMING_TC_REG21_HMIRROR = const(0x06)
# Horizontal mirror enable

OV5640_SIZE_96X96 = 0  # 96x96
OV5640_SIZE_QQVGA = 1  # 160x120
OV5640_SIZE_QCIF = 2  # 176x144
OV5640_SIZE_HQVGA = 3  # 240x176
OV5640_SIZE_240X240 = 4  # 240x240
OV5640_SIZE_QVGA = 5  # 320x240
OV5640_SIZE_CIF = 6  # 400x296
OV5640_SIZE_HVGA = 7  # 480x320
OV5640_SIZE_VGA = 8  # 640x480
OV5640_SIZE_SVGA = 9  # 800x600
OV5640_SIZE_XGA = 10  # 1024x768
OV5640_SIZE_HD = 11  # 1280x720
OV5640_SIZE_SXGA = 12  # 1280x1024
OV5640_SIZE_UXGA = 13  # 1600x1200
OV5640_SIZE_QHDA = 14  # 2560x1440
OV5640_SIZE_WQXGA = 15  # 2560x1600
OV5640_SIZE_PFHD = 16  # 1088x1920
OV5640_SIZE_QSXGA = 17  # 2560x1920

_ASPECT_RATIO_4X3 = const(0)
_ASPECT_RATIO_3X2 = const(1)
_ASPECT_RATIO_16X10 = const(2)
_ASPECT_RATIO_5X3 = const(3)
_ASPECT_RATIO_16X9 = const(4)
_ASPECT_RATIO_21X9 = const(5)
_ASPECT_RATIO_5X4 = const(6)
_ASPECT_RATIO_1X1 = const(7)
_ASPECT_RATIO_9X16 = const(8)

_resolution_info = [
    [96, 96, _ASPECT_RATIO_1X1],  # 96x96
    [160, 120, _ASPECT_RATIO_4X3],  # QQVGA
    [176, 144, _ASPECT_RATIO_5X4],  # QCIF
    [240, 176, _ASPECT_RATIO_4X3],  # HQVGA
    [240, 240, _ASPECT_RATIO_1X1],  # 240x240
    [320, 240, _ASPECT_RATIO_4X3],  # QVGA
    [400, 296, _ASPECT_RATIO_4X3],  # CIF
    [480, 320, _ASPECT_RATIO_3X2],  # HVGA
    [640, 480, _ASPECT_RATIO_4X3],  # VGA
    [800, 600, _ASPECT_RATIO_4X3],  # SVGA
    [1024, 768, _ASPECT_RATIO_4X3],  # XGA
    [1280, 720, _ASPECT_RATIO_16X9],  # HD
    [1280, 1024, _ASPECT_RATIO_5X4],  # SXGA
    [1600, 1200, _ASPECT_RATIO_4X3],  # UXGA
    [2560, 1440, _ASPECT_RATIO_16X9], # QHD
    [2560, 1600, _ASPECT_RATIO_16X10], # WQXGA
    [1088, 1920, _ASPECT_RATIO_9X16], # Portrait FHD
    [2560, 1920, _ASPECT_RATIO_4X3], # QSXGA

]


_ratio_table = [
    #  mw,   mh,  sx,  sy,   ex,   ey, ox, oy,   tx,   ty
    [2560, 1920, 0, 0, 2623, 1951, 32, 16, 2844, 1968],  # 4x3
    [2560, 1704, 0, 110, 2623, 1843, 32, 16, 2844, 1752],  # 3x2
    [2560, 1600, 0, 160, 2623, 1791, 32, 16, 2844, 1648],  # 16x10
    [2560, 1536, 0, 192, 2623, 1759, 32, 16, 2844, 1584],  # 5x3
    [2560, 1440, 0, 240, 2623, 1711, 32, 16, 2844, 1488],  # 16x9
    [2560, 1080, 0, 420, 2623, 1531, 32, 16, 2844, 1128],  # 21x9
    [2400, 1920, 80, 0, 2543, 1951, 32, 16, 2684, 1968],  # 5x4
    [1920, 1920, 320, 0, 2543, 1951, 32, 16, 2684, 1968],  # 1x1
    [1088, 1920, 736, 0, 1887, 1951, 32, 16, 1884, 1968],  # 9x16
]

_pll_pre_div2x_factors = [1, 1, 2, 3, 4, 1.5, 6, 2.5, 8]
_pll_pclk_root_div_factors = [1,2,4,8]

_REG_DLY = const(0xFFFF)
_REGLIST_TAIL = const(0x0000)

_sensor_default_regs = [
    _SYSTEM_CTROL0, 0x82,  # software reset
    _REG_DLY, 10,  # delay 10ms
    _SYSTEM_CTROL0, 0x42,  # power down
    # enable pll
    0x3103, 0x13,
    # io direction
    0x3017, 0xFF,
    0x3018, 0xFF,
    _DRIVE_CAPABILITY, 0xC3,
    _CLOCK_POL_CONTROL, 0x21,
    0x4713, 0x02,  # jpg mode select
    _ISP_CONTROL_01, 0x83,  # turn color matrix, awb and SDE
    # sys reset
    _SYSTEM_RESET00, 0x00, # enable all blocks
    _SYSTEM_RESET02, 0x1C, # reset jfifo, sfifo, jpg, fmux, avg
    # clock enable
    0x3004, 0xFF,
    _CLOCK_ENABLE02, 0xC3,
    # isp control
    0x5000, 0xA7,
    _ISP_CONTROL_01, 0xA3,  # +scaling?
    0x5003, 0x08,  # special_effect
    # unknown
    0x370C, 0x02,  #!!IMPORTANT
    0x3634, 0x40,  #!!IMPORTANT
    # AEC/AGC
    0x3A02, 0x03,
    0x3A03, 0xD8,
    0x3A08, 0x01,
    0x3A09, 0x27,
    0x3A0A, 0x00,
    0x3A0B, 0xF6,
    0x3A0D, 0x04,
    0x3A0E, 0x03,
    0x3A0F, 0x30,  # ae_level
    0x3A10, 0x28,  # ae_level
    0x3A11, 0x60,  # ae_level
    0x3A13, 0x43,
    0x3A14, 0x03,
    0x3A15, 0xD8,
    0x3A18, 0x00,  # gainceiling
    0x3A19, 0xF8,  # gainceiling
    0x3A1B, 0x30,  # ae_level
    0x3A1E, 0x26,  # ae_level
    0x3A1F, 0x14,  # ae_level
    # vcm debug
    0x3600, 0x08,
    0x3601, 0x33,
    # 50/60Hz
    0x3C01, 0xA4,
    0x3C04, 0x28,
    0x3C05, 0x98,
    0x3C06, 0x00,
    0x3C07, 0x08,
    0x3C08, 0x00,
    0x3C09, 0x1C,
    0x3C0A, 0x9C,
    0x3C0B, 0x40,
    0x460C, 0x22,  # disable jpeg footer
    # BLC
    0x4001, 0x02,
    0x4004, 0x02,
    # AWB
    0x5180, 0xFF,
    0x5181, 0xF2,
    0x5182, 0x00,
    0x5183, 0x14,
    0x5184, 0x25,
    0x5185, 0x24,
    0x5186, 0x09,
    0x5187, 0x09,
    0x5188, 0x09,
    0x5189, 0x75,
    0x518A, 0x54,
    0x518B, 0xE0,
    0x518C, 0xB2,
    0x518D, 0x42,
    0x518E, 0x3D,
    0x518F, 0x56,
    0x5190, 0x46,
    0x5191, 0xF8,
    0x5192, 0x04,
    0x5193, 0x70,
    0x5194, 0xF0,
    0x5195, 0xF0,
    0x5196, 0x03,
    0x5197, 0x01,
    0x5198, 0x04,
    0x5199, 0x12,
    0x519A, 0x04,
    0x519B, 0x00,
    0x519C, 0x06,
    0x519D, 0x82,
    0x519E, 0x38,
    # color matrix (Saturation)
    0x5381, 0x1E,
    0x5382, 0x5B,
    0x5383, 0x08,
    0x5384, 0x0A,
    0x5385, 0x7E,
    0x5386, 0x88,
    0x5387, 0x7C,
    0x5388, 0x6C,
    0x5389, 0x10,
    0x538A, 0x01,
    0x538B, 0x98,
    # CIP control (Sharpness)
    0x5300, 0x10,  # sharpness
    0x5301, 0x10,  # sharpness
    0x5302, 0x18,  # sharpness
    0x5303, 0x19,  # sharpness
    0x5304, 0x10,
    0x5305, 0x10,
    0x5306, 0x08,  # denoise
    0x5307, 0x16,
    0x5308, 0x40,
    0x5309, 0x10,  # sharpness
    0x530A, 0x10,  # sharpness
    0x530B, 0x04,  # sharpness
    0x530C, 0x06,  # sharpness
    # GAMMA
    0x5480, 0x01,
    0x5481, 0x00,
    0x5482, 0x1E,
    0x5483, 0x3B,
    0x5484, 0x58,
    0x5485, 0x66,
    0x5486, 0x71,
    0x5487, 0x7D,
    0x5488, 0x83,
    0x5489, 0x8F,
    0x548A, 0x98,
    0x548B, 0xA6,
    0x548C, 0xB8,
    0x548D, 0xCA,
    0x548E, 0xD7,
    0x548F, 0xE3,
    0x5490, 0x1D,
    # Special Digital Effects (SDE) (UV adjust)
    0x5580, 0x06,  # enable brightness and contrast
    0x5583, 0x40,  # special_effect
    0x5584, 0x10,  # special_effect
    0x5586, 0x20,  # contrast
    0x5587, 0x00,  # brightness
    0x5588, 0x00,  # brightness
    0x5589, 0x10,
    0x558A, 0x00,
    0x558B, 0xF8,
    0x501D, 0x40,  # enable manual offset of contrast
    # power on
    0x3008, 0x02,
    # 50Hz
    0x3C00, 0x04,
    #_REG_DLY, 300,
]



_reset_awb = [
    _ISP_CONTROL_01, 0x83,  # turn color matrix, awb and SDE
    # sys reset
    _SYSTEM_RESET00, 0x00, # enable all blocks
    _SYSTEM_RESET02, 0x1C, # reset jfifo, sfifo, jpg, fmux, avg
    # clock enable
    #0x3004, 0xFF,
    #_CLOCK_ENABLE02, 0xC3,
    # isp control
    0x5000, 0xA7,
    _ISP_CONTROL_01, 0xA3,  # +scaling?
    0x5003, 0x08,  # special_effect
    # unknown
    0x370C, 0x02,  #!!IMPORTANT
    0x3634, 0x40,  #!!IMPORTANT
    # AEC/AGC
    0x3A02, 0x03,
    0x3A03, 0xD8,
    0x3A08, 0x01,
    0x3A09, 0x27,
    0x3A0A, 0x00,
    0x3A0B, 0xF6,
    0x3A0D, 0x04,
    0x3A0E, 0x03,
    0x3A0F, 0x30,  # ae_level
    0x3A10, 0x28,  # ae_level
    0x3A11, 0x60,  # ae_level
    0x3A13, 0x43,
    0x3A14, 0x03,
    0x3A15, 0xD8,
    0x3A18, 0x00,  # gainceiling
    0x3A19, 0xF8,  # gainceiling
    0x3A1B, 0x30,  # ae_level
    0x3A1E, 0x26,  # ae_level
    0x3A1F, 0x14,  # ae_level
    # vcm debug
    0x3600, 0x08,
    0x3601, 0x33,
    # 50/60Hz
    0x3C01, 0xA4,
    0x3C04, 0x28,
    0x3C05, 0x98,
    0x3C06, 0x00,
    0x3C07, 0x08,
    0x3C08, 0x00,
    0x3C09, 0x1C,
    0x3C0A, 0x9C,
    0x3C0B, 0x40,
    0x460C, 0x22,  # disable jpeg footer
    # BLC
    0x4001, 0x02,
    0x4004, 0x02,
    # AWB
    0x5180, 0xFF,
    0x5181, 0xF2,
    0x5182, 0x00,
    0x5183, 0x14,
    0x5184, 0x25,
    0x5185, 0x24,
    0x5186, 0x09,
    0x5187, 0x09,
    0x5188, 0x09,
    0x5189, 0x75,
    0x518A, 0x54,
    0x518B, 0xE0,
    0x518C, 0xB2,
    0x518D, 0x42,
    0x518E, 0x3D,
    0x518F, 0x56,
    0x5190, 0x46,
    0x5191, 0xF8,
    0x5192, 0x04,
    0x5193, 0x70,
    0x5194, 0xF0,
    0x5195, 0xF0,
    0x5196, 0x03,
    0x5197, 0x01,
    0x5198, 0x04,
    0x5199, 0x12,
    0x519A, 0x04,
    0x519B, 0x00,
    0x519C, 0x06,
    0x519D, 0x82,
    0x519E, 0x38,
    # color matrix (Saturation)
    0x5381, 0x1E,
    0x5382, 0x5B,
    0x5383, 0x08,
    0x5384, 0x0A,
    0x5385, 0x7E,
    0x5386, 0x88,
    0x5387, 0x7C,
    0x5388, 0x6C,
    0x5389, 0x10,
    0x538A, 0x01,
    0x538B, 0x98,
    # CIP control (Sharpness)
    0x5300, 0x10,  # sharpness
    0x5301, 0x10,  # sharpness
    0x5302, 0x18,  # sharpness
    0x5303, 0x19,  # sharpness
    0x5304, 0x10,
    0x5305, 0x10,
    0x5306, 0x08,  # denoise
    0x5307, 0x16,
    0x5308, 0x40,
    0x5309, 0x10,  # sharpness
    0x530A, 0x10,  # sharpness
    0x530B, 0x04,  # sharpness
    0x530C, 0x06,  # sharpness
    # GAMMA
    0x5480, 0x01,
    0x5481, 0x00,
    0x5482, 0x1E,
    0x5483, 0x3B,
    0x5484, 0x58,
    0x5485, 0x66,
    0x5486, 0x71,
    0x5487, 0x7D,
    0x5488, 0x83,
    0x5489, 0x8F,
    0x548A, 0x98,
    0x548B, 0xA6,
    0x548C, 0xB8,
    0x548D, 0xCA,
    0x548E, 0xD7,
    0x548F, 0xE3,
    0x5490, 0x1D,
    # Special Digital Effects (SDE) (UV adjust)
    0x5580, 0x06,  # enable brightness and contrast
    0x5583, 0x40,  # special_effect
    0x5584, 0x10,  # special_effect
    0x5586, 0x20,  # contrast
    0x5587, 0x00,  # brightness
    0x5588, 0x00,  # brightness
    0x5589, 0x10,
    0x558A, 0x00,
    0x558B, 0xF8,
    0x501D, 0x40,  # enable manual offset of contrast
]
_sensor_format_jpeg = [
    _FORMAT_CTRL, 0x00,  # YUV422
    _FORMAT_CTRL00, 0x30,  # YUYV
    _SYSTEM_RESET02, 0x00,  # enable everything
    _CLOCK_ENABLE02, 0xFF,  # enable all clocks
    0x471C, 0x50,  # 0xd0 to 0x50 !!!
]

_sensor_format_raw = [
    _FORMAT_CTRL, 0x03,  # RAW (DPC)
    _FORMAT_CTRL00, 0x00,  # RAW
]

_sensor_format_grayscale = [
    _FORMAT_CTRL, 0x00,  # YUV422
    _FORMAT_CTRL00, 0x10,  # Y8
]

_sensor_format_yuv422 = [
    _FORMAT_CTRL, 0x00,  # YUV422
    _FORMAT_CTRL00, 0x30,  # YUYV
]

_sensor_format_rgb565 = [
    _FORMAT_CTRL, 0x01,  # RGB
    _FORMAT_CTRL00, 0x61,  # RGB565 (BGR)
    _SYSTEM_RESET02, 0x1C, # reset jfifo, sfifo, jpg, fmux, avg
    _CLOCK_ENABLE02, 0xC3, # reset to how it was before (no jpg clock)

]

_ov5640_color_settings = {
    OV5640_COLOR_RGB: _sensor_format_rgb565,
    OV5640_COLOR_YUV: _sensor_format_yuv422,
    OV5640_COLOR_GRAYSCALE: _sensor_format_grayscale,
    OV5640_COLOR_JPEG: _sensor_format_jpeg,
}

_contrast_settings = [
    [0x20, 0x00], #  0
    [0x24, 0x10], # +1
    [0x28, 0x18], # +2
    [0x2c, 0x1c], # +3
    [0x14, 0x14], # -3
    [0x18, 0x18], # -2
    [0x1c, 0x1c], # -1
]

_sensor_saturation_levels = [
    [0x1D, 0x60, 0x03, 0x0C, 0x78, 0x84, 0x7D, 0x6B, 0x12, 0x01, 0x98],  # 0
    [0x1D, 0x60, 0x03, 0x0D, 0x84, 0x91, 0x8A, 0x76, 0x14, 0x01, 0x98],  # +1
    [0x1D, 0x60, 0x03, 0x0E, 0x90, 0x9E, 0x96, 0x80, 0x16, 0x01, 0x98],  # +2
    [0x1D, 0x60, 0x03, 0x10, 0x9C, 0xAC, 0xA2, 0x8B, 0x17, 0x01, 0x98],  # +3
    [0x1D, 0x60, 0x03, 0x11, 0xA8, 0xB9, 0xAF, 0x96, 0x19, 0x01, 0x98],  # +4
    [0x1D, 0x60, 0x03, 0x07, 0x48, 0x4F, 0x4B, 0x40, 0x0B, 0x01, 0x98],  # -4
    [0x1D, 0x60, 0x03, 0x08, 0x54, 0x5C, 0x58, 0x4B, 0x0D, 0x01, 0x98],  # -3
    [0x1D, 0x60, 0x03, 0x0A, 0x60, 0x6A, 0x64, 0x56, 0x0E, 0x01, 0x98],  # -2
    [0x1D, 0x60, 0x03, 0x0B, 0x6C, 0x77, 0x70, 0x60, 0x10, 0x01, 0x98],  # -1
]

_sensor_ev_levels = [
    [0x38, 0x30, 0x61, 0x38, 0x30, 0x10], #  0
    [0x40, 0x38, 0x71, 0x40, 0x38, 0x10], # +1
    [0x50, 0x48, 0x90, 0x50, 0x48, 0x20], # +2
    [0x60, 0x58, 0xa0, 0x60, 0x58, 0x20], # +3
    [0x10, 0x08, 0x10, 0x08, 0x20, 0x10], # -3
    [0x20, 0x18, 0x41, 0x20, 0x18, 0x10], # -2
    [0x30, 0x28, 0x61, 0x30, 0x28, 0x10], # -1
]

OV5640_WHITE_BALANCE_AUTO = 0
OV5640_WHITE_BALANCE_SUNNY = 1
OV5640_WHITE_BALANCE_FLUORESCENT = 2
OV5640_WHITE_BALANCE_CLOUDY = 3
OV5640_WHITE_BALANCE_INCANDESCENT = 4

_light_registers = [0x3406, 0x3400, 0x3401, 0x3402, 0x3403, 0x3404, 0x3405]
_light_modes = [
    [0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00], # auto
    [0x01, 0x06, 0x1c, 0x04, 0x00, 0x04, 0xf3], # sunny
    [0x01, 0x05, 0x48, 0x04, 0x00, 0x07, 0xcf], # office / fluorescent
    [0x01, 0x06, 0x48, 0x04, 0x00, 0x04, 0xd3], # cloudy
    [0x01, 0x04, 0x10, 0x04, 0x00, 0x08, 0x40], # home / incandescent

]

OV5640_SPECIAL_EFFECT_NONE = 0
OV5640_SPECIAL_EFFECT_NEGATIVE = 1
OV5640_SPECIAL_EFFECT_GRAYSCALE = 2
OV5640_SPECIAL_EFFECT_RED_TINT = 3
OV5640_SPECIAL_EFFECT_GREEN_TINT = 4
OV5640_SPECIAL_EFFECT_BLUE_TINT = 5
OV5640_SPECIAL_EFFECT_SEPIA = 6

_sensor_special_effects = [
    [0x06, 0x40, 0x10, 0x08],  # Normal
    [0x46, 0x40, 0x28, 0x08],  # Negative
    [0x1E, 0x80, 0x80, 0x08],  # Grayscale
    [0x1E, 0x80, 0xC0, 0x08],  # Red Tint
    [0x1E, 0x60, 0x60, 0x08],  # Green Tint
    [0x1E, 0xA0, 0x40, 0x08],  # Blue Tint
    [0x1E, 0x40, 0xA0, 0x08],  # Sepia
]

_sensor_regs_gamma0 = [
    0x5480, 0x01,
    0x5481, 0x08,
    0x5482, 0x14,
    0x5483, 0x28,
    0x5484, 0x51,
    0x5485, 0x65,
    0x5486, 0x71,
    0x5487, 0x7D,
    0x5488, 0x87,
    0x5489, 0x91,
    0x548A, 0x9A,
    0x548B, 0xAA,
    0x548C, 0xB8,
    0x548D, 0xCD,
    0x548E, 0xDD,
    0x548F, 0xEA,
    0x5490, 0x1D,
]

sensor_regs_gamma1 = [
    0x5480, 0x1,
    0x5481, 0x0,
    0x5482, 0x1E,
    0x5483, 0x3B,
    0x5484, 0x58,
    0x5485, 0x66,
    0x5486, 0x71,
    0x5487, 0x7D,
    0x5488, 0x83,
    0x5489, 0x8F,
    0x548A, 0x98,
    0x548B, 0xA6,
    0x548C, 0xB8,
    0x548D, 0xCA,
    0x548E, 0xD7,
    0x548F, 0xE3,
    0x5490, 0x1D,
]

sensor_regs_awb0 = [
    0x5180, 0xFF,
    0x5181, 0xF2,
    0x5182, 0x00,
    0x5183, 0x14,
    0x5184, 0x25,
    0x5185, 0x24,
    0x5186, 0x09,
    0x5187, 0x09,
    0x5188, 0x09,
    0x5189, 0x75,
    0x518A, 0x54,
    0x518B, 0xE0,
    0x518C, 0xB2,
    0x518D, 0x42,
    0x518E, 0x3D,
    0x518F, 0x56,
    0x5190, 0x46,
    0x5191, 0xF8,
    0x5192, 0x04,
    0x5193, 0x70,
    0x5194, 0xF0,
    0x5195, 0xF0,
    0x5196, 0x03,
    0x5197, 0x01,
    0x5198, 0x04,
    0x5199, 0x12,
    0x519A, 0x04,
    0x519B, 0x00,
    0x519C, 0x06,
    0x519D, 0x82,
    0x519E, 0x38,
]
# fmt: on


class _RegBits:
    def __init__(self, reg: int, shift: int, mask: int) -> None:
        self.reg = reg
        self.shift = shift
        self.mask = mask

    def __get__(self, obj: "OV5640", objtype: Optional[type] = None) -> int:
        reg_value = obj._read_register(self.reg)
        return (reg_value >> self.shift) & self.mask

    def __set__(self, obj: "OV5640", value: int) -> None:
        if value & ~self.mask:
            raise ValueError(
                f"Value 0x{value:02x} does not fit in mask 0x{self.mask:02x}"
            )
        reg_value = obj._read_register(self.reg)
        reg_value &= ~(self.mask << self.shift)
        reg_value |= value << self.shift
        obj._write_register(self.reg, reg_value)


class _RegBits16:
    def __init__(self, reg: int, shift: int, mask: int) -> None:
        self.reg = reg
        self.shift = shift
        self.mask = mask

    def __get__(self, obj: "OV5640", objtype: Optional[type] = None) -> int:
        reg_value = obj._read_register16(self.reg)
        return (reg_value >> self.shift) & self.mask

    def __set__(self, obj: "OV5640", value: int) -> None:
        if value & ~self.mask:
            raise ValueError(
                f"Value 0x{value:02x} does not fit in mask 0x{self.mask:02x}"
            )
        reg_value = obj._read_register16(self.reg)
        reg_value &= ~(self.mask << self.shift)
        reg_value |= value << self.shift
        obj._write_register16(self.reg, reg_value)


class _SCCB16CameraBase:  # pylint: disable=too-few-public-methods
    def __init__(self, i2c_bus: I2C, i2c_address: int) -> None:
        self._i2c_device = I2CDevice(i2c_bus, i2c_address)
        self._bank = None

    def _write_register(self, reg: int, value: int) -> None:
        b = bytearray(3)
        b[0] = reg >> 8
        b[1] = reg & 0xFF
        b[2] = value
        with self._i2c_device as i2c:
            i2c.write(b)

    def _write_addr_reg(self, reg: int, x_value: int, y_value: int) -> None:
        self._write_register16(reg, x_value)
        self._write_register16(reg + 2, y_value)

    def _write_register16(self, reg: int, value: int) -> None:
        self._write_register(reg, value >> 8)
        self._write_register(reg + 1, value & 0xFF)

    def _read_register(self, reg: int) -> int:
        b = bytearray(2)
        b[0] = reg >> 8
        b[1] = reg & 0xFF
        with self._i2c_device as i2c:
            i2c.write(b)
            i2c.readinto(b, end=1)
        return b[0]

    def _read_register16(self, reg: int) -> int:
        high = self._read_register(reg)
        low = self._read_register(reg + 1)
        return (high << 8) | low

    def _write_list(self, reg_list: Sequence[int]) -> None:
        for i in range(0, len(reg_list), 2):
            register = reg_list[i]
            value = reg_list[i + 1]
            if register == _REG_DLY:
                time.sleep(value / 1000)
            else:
                self._write_register(register, value)

    def _write_reg_bits(self, reg: int, mask: int, enable: bool) -> None:
        val = val = self._read_register(reg)
        if enable:
            val |= mask
        else:
            val &= ~mask
        self._write_register(reg, val)


class OV5640(_SCCB16CameraBase):  # pylint: disable=too-many-instance-attributes
    """Control & Capture Images from an OV5640 Camera"""

    def __init__(
        self,
        i2c_bus: I2C,
        data_pins: List[Pin],
        clock: Pin,
        vsync: Pin,
        href: Pin,
        shutdown: Optional[DigitalInOut] = None,
        reset: Optional[DigitalInOut] = None,
        mclk: Optional[Pin] = None,
        mclk_frequency: int = 20_000_000,
        i2c_address: int = 0x3C,
        size: int = OV5640_SIZE_QQVGA,
    ):  # pylint: disable=too-many-arguments
        """
        Args:
            i2c_bus (busio.I2C): The I2C bus used to configure the OV5640
            data_pins (List[microcontroller.Pin]): A list of 8 data pins, in order.
            clock (microcontroller.Pin): The pixel clock from the OV5640.
            vsync (microcontroller.Pin): The vsync signal from the OV5640.
            href (microcontroller.Pin): The href signal from the OV5640, \
                sometimes inaccurately called hsync.
            shutdown (Optional[digitalio.DigitalInOut]): If not None, the shutdown
                signal to the camera, also called the powerdown or enable pin.
            reset (Optional[digitalio.DigitalInOut]): If not None, the reset signal
                to the camera.
            mclk (Optional[microcontroller.Pin]): The pin on which to create a
                master clock signal, or None if the master clock signal is
                already being generated.
            mclk_frequency (int): The frequency of the master clock to generate, \
                ignored if mclk is None, requred if it is specified.
                Note that the OV5640 requires a very low jitter clock,
                so only specific (microcontroller-dependent) values may
                work reliably.  On the ESP32-S2, a 20MHz clock can be generated
                with sufficiently low jitter.
            i2c_address (int): The I2C address of the camera.
            size (int): The captured image size
        """

        # Initialize the master clock
        if mclk:
            self._mclk_pwm = pwmio.PWMOut(mclk, frequency=mclk_frequency)
            self._mclk_pwm.duty_cycle = 32768
        else:
            self._mclk_pwm = None

        if reset:
            self._reset = reset
            self._reset.switch_to_output(False)
        else:
            self._reset = None

        if shutdown:
            self._shutdown = shutdown
            self._shutdown.switch_to_output(True)
            time.sleep(0.005)  # t2, 5ms stability
            self._shutdown.switch_to_output(False)
        else:
            self._shutdown = None

        if self._reset:
            time.sleep(0.001)  # t3, 1ms delay from pwdn
            self._reset.switch_to_output(True)
            time.sleep(0.02)

        # Now that the master clock is running, we can initialize i2c comms
        super().__init__(i2c_bus, i2c_address)

        self._write_list(_sensor_default_regs)

        self._imagecapture = imagecapture.ParallelImageCapture(
            data_pins=data_pins, clock=clock, vsync=vsync, href=href
        )

        self._colorspace = OV5640_COLOR_RGB
        self._flip_x = False
        self._flip_y = False
        self._w = None
        self._h = None
        self._size = None
        self._test_pattern = False
        self._binning = False
        self._scale = False
        self._ev = 0
        self._white_balance = 0
        self.size = size

    chip_id = _RegBits16(_CHIP_ID_HIGH, 0, 0xFFFF)

    def capture(self, buf: Union[bytearray, memoryview]) -> None:
        """Capture an image into the buffer.

        Args:
            buf (Union[bytearray, memoryview]): A WritableBuffer to contain the \
                captured image.  Note that this can be a ulab array or a displayio Bitmap.
        """
        self._imagecapture.capture(buf)
        if self.colorspace == OV5640_COLOR_JPEG:
            eoi = buf.find(b"\xff\xd9")
            if eoi != -1:
                # terminate the JPEG data just after the EOI marker
                return memoryview(buf)[: eoi + 2]
        return None

    @property
    def capture_buffer_size(self) -> int:
        """Return the size of capture buffer to use with current resolution & colorspace settings"""
        if self.colorspace == OV5640_COLOR_JPEG:
            return self.width * self.height // self.quality
        if self.colorspace == OV5640_COLOR_GRAYSCALE:
            return self.width * self.height
        return self.width * self.height * 2

    @property
    def mclk_frequency(self) -> Optional[int]:
        """Get the actual frequency the generated mclk, or None"""
        return self._mclk_pwm.frequency if self._mclk_pwm else None

    @property
    def width(self) -> int:
        """Get the image width in pixels."""
        return self._w

    @property
    def height(self) -> int:
        """Get the image height in pixels."""
        return self._h

    @property
    def colorspace(self) -> int:
        """Get or set the colorspace, one of the ``OV5640_COLOR_`` constants."""
        return self._colorspace

    @colorspace.setter
    def colorspace(self, colorspace: int) -> None:
        self._colorspace = colorspace
        self._set_size_and_colorspace()

    def _set_image_options(self) -> None:  # pylint: disable=too-many-branches
        reg20 = reg21 = reg4514 = reg4514_test = 0
        if self.colorspace == OV5640_COLOR_JPEG:
            reg21 |= 0x20

        if self._binning:
            reg20 |= 1
            reg21 |= 1
            reg4514_test |= 4
        else:
            reg20 |= 0x40

        if self._flip_y:
            reg20 |= 0x06
            reg4514_test |= 1

        if self._flip_x:
            reg21 |= 0x06
            reg4514_test |= 2

        if reg4514_test == 0:
            reg4514 = 0x88
        elif reg4514_test == 1:
            reg4514 = 0x00
        elif reg4514_test == 2:
            reg4514 = 0xBB
        elif reg4514_test == 3:
            reg4514 = 0x00
        elif reg4514_test == 4:
            reg4514 = 0xAA
        elif reg4514_test == 5:
            reg4514 = 0xBB
        elif reg4514_test == 6:
            reg4514 = 0xBB
        elif reg4514_test == 7:
            reg4514 = 0xAA

        self._write_register(_TIMING_TC_REG20, reg20)
        self._write_register(_TIMING_TC_REG21, reg21)
        self._write_register(0x4514, reg4514)

        if self._binning:
            self._write_register(0x4520, 0x0B)
            self._write_register(_X_INCREMENT, 0x31)
            self._write_register(_Y_INCREMENT, 0x31)
        else:
            self._write_register(0x4520, 0x10)
            self._write_register(_X_INCREMENT, 0x11)
            self._write_register(_Y_INCREMENT, 0x11)

    def _set_colorspace(self) -> None:
        colorspace = self._colorspace
        settings = _ov5640_color_settings[colorspace]

        self._write_list(settings)

    def deinit(self) -> None:
        """Deinitialize the camera"""
        self._imagecapture.deinit()
        if self._mclk_pwm:
            self._mclk_pwm.deinit()
        if self._shutdown:
            self._shutdown.deinit()
        if self._reset:
            self._reset.deinit()

    @property
    def size(self) -> int:
        """Get or set the captured image size, one of the ``OV5640_SIZE_`` constants."""
        return self._size

    def _set_size_and_colorspace(self) -> None:  # pylint: disable=too-many-locals
        size = self._size
        width, height, ratio = _resolution_info[size]
        self._w = width
        self._h = height
        (
            max_width,
            max_height,
            start_x,
            start_y,
            end_x,
            end_y,
            offset_x,
            offset_y,
            total_x,
            total_y,
        ) = _ratio_table[ratio]

        self._binning = (width <= max_width // 2) and (height <= max_height // 2)
        self._scale = not (
            (width == max_width and height == max_height)
            or (width == max_width // 2 and height == max_height // 2)
        )

        self._write_addr_reg(_X_ADDR_ST_H, start_x, start_y)
        self._write_addr_reg(_X_ADDR_END_H, end_x, end_y)
        self._write_addr_reg(_X_OUTPUT_SIZE_H, width, height)

        if not self._binning:
            self._write_addr_reg(_X_TOTAL_SIZE_H, total_x, total_y)
            self._write_addr_reg(_X_OFFSET_H, offset_x, offset_y)
        else:
            if width > 920:
                self._write_addr_reg(_X_TOTAL_SIZE_H, total_x - 200, total_y // 2)
            else:
                self._write_addr_reg(_X_TOTAL_SIZE_H, 2060, total_y // 2)
            self._write_addr_reg(_X_OFFSET_H, offset_x // 2, offset_y // 2)

        self._write_reg_bits(_ISP_CONTROL_01, 0x20, self._scale)

        self._set_image_options()

        if self.colorspace == OV5640_COLOR_JPEG:
            sys_mul = 200
            if size < OV5640_SIZE_QVGA:
                sys_mul = 160
            if size < OV5640_SIZE_XGA:
                sys_mul = 180
            self._set_pll(False, sys_mul, 4, 2, False, 2, True, 4)
        else:
            self._set_pll(False, 32, 1, 1, False, 1, True, 4)

        self._set_colorspace()

    def _set_pll(  # pylint: disable=too-many-arguments
        self,
        bypass: bool,
        multiplier: int,
        sys_div: int,
        pre_div: int,
        root_2x: bool,
        pclk_root_div: int,
        pclk_manual: bool,
        pclk_div: int,
    ) -> None:
        if (  # pylint: disable=too-many-boolean-expressions
            multiplier > 252
            or multiplier < 4
            or sys_div > 15
            or pre_div > 8
            or pclk_div > 31
            or pclk_root_div > 3
        ):
            raise ValueError("Invalid argument to internal function")

        self._write_register(0x3039, 0x80 if bypass else 0)
        self._write_register(0x3034, 0x1A)
        self._write_register(0x3035, 1 | ((sys_div & 0xF) << 4))
        self._write_register(0x3036, multiplier & 0xFF)
        self._write_register(0x3037, (pre_div & 0xF) | (0x10 if root_2x else 0))
        self._write_register(0x3108, (pclk_root_div & 3) << 4 | 0x06)
        self._write_register(0x3824, pclk_div & 0x1F)
        self._write_register(0x460C, 0x22 if pclk_manual else 0x22)
        self._write_register(0x3103, 0x13)

    @size.setter
    def size(self, size: int) -> None:
        self._size = size
        self._set_size_and_colorspace()

    @property
    def flip_x(self) -> bool:
        """Get or set the X-flip flag"""
        return self._flip_x

    @flip_x.setter
    def flip_x(self, value: bool) -> None:
        self._flip_x = bool(value)
        self._set_image_options()

    @property
    def flip_y(self) -> bool:
        """Get or set the Y-flip flag"""
        return self._flip_y

    @flip_y.setter
    def flip_y(self, value: bool) -> None:
        self._flip_y = bool(value)
        self._set_image_options()

    @property
    def test_pattern(self) -> bool:
        """Set to True to enable a test pattern, False to enable normal image capture"""
        return self._test_pattern

    @test_pattern.setter
    def test_pattern(self, value: bool) -> None:
        self._test_pattern = value
        self._write_register(_PRE_ISP_TEST_SETTING_1, value << 7)

    @property
    def saturation(self) -> int:
        """Get or set the saturation value, from -4 to +4."""
        return self._saturation

    @saturation.setter
    def saturation(self, value: int) -> None:
        if not -4 <= value <= 4:
            raise ValueError(
                "Invalid saturation {value}, use a value from -4..4 inclusive"
            )
        for offset, reg_value in enumerate(_sensor_saturation_levels[value]):
            self._write_register(0x5381 + offset, reg_value)
        self._saturation = value

    @property
    def effect(self) -> int:
        """Get or set the special effect, one of the ``OV5640_SPECIAL_EFFECT_`` constants"""
        return self._effect

    @effect.setter
    def effect(self, value: int) -> None:
        for reg_addr, reg_value in zip(
            (0x5580, 0x5583, 0x5584, 0x5003), _sensor_special_effects[value]
        ):
            self._write_register(reg_addr, reg_value)
        self._effect = value

    @property
    def quality(self) -> int:
        """Controls the JPEG quality.  Valid range is from 2..55 inclusive"""
        return self._read_register(_COMPRESSION_CTRL07) & 0x3F

    @quality.setter
    def quality(self, value: int) -> None:
        if not 2 <= value < 55:
            raise ValueError(
                f"Invalid quality value {value}, use a value from 2..55 inclusive"
            )
        self._write_register(_COMPRESSION_CTRL07, value & 0x3F)

    def _write_group_3_settings(self, settings):
        self._write_register(0x3212, 0x3)  # start group 3
        self._write_list(settings)
        self._write_register(0x3212, 0x13)  # end group 3
        self._write_register(0x3212, 0xA3)  # launch group 3

    @property
    def brightness(self) -> int:
        """Sensor brightness adjustment, from -4 to 4 inclusive"""
        brightness_abs = self._read_register(0x5587) >> 4
        brightness_neg = self._read_register(0x5588) & 8
        if brightness_neg:
            return -brightness_abs
        return brightness_abs

    @brightness.setter
    def brightness(self, value: int) -> None:
        if not -4 <= value <= 4:
            raise ValueError(
                "Invalid brightness value {value}, use a value from -4..4 inclusive"
            )
        self._write_group_3_settings(
            [0x5587, abs(value) << 4, 0x5588, 0x9 if value < 0 else 0x1]
        )

    @property
    def contrast(self) -> int:
        """Sensor contrast adjustment, from -4 to 4 inclusive"""
        contrast_abs = self._read_register(0x5587) >> 4
        contrast_neg = self._read_register(0x5588) & 8
        if contrast_neg:
            return -contrast_abs
        return contrast_abs

    @contrast.setter
    def contrast(self, value: int) -> None:
        if not -3 <= value <= 3:
            raise ValueError(
                "Invalid contrast value {value}, use a value from -3..3 inclusive"
            )
        setting = _contrast_settings[value]
        self._write_group_3_settings([0x5586, setting[0], 0x5585, setting[1]])

    @property
    def exposure_value(self) -> int:
        """Sensor exposure (EV) adjustment, from -4 to 4 inclusive"""
        return self._ev

    @exposure_value.setter
    def exposure_value(self, value: int) -> None:
        if not -3 <= value <= 3:
            raise ValueError(
                "Invalid exposure value (EV) {value}, use a value from -4..4 inclusive"
            )
        for offset, reg_value in enumerate(_sensor_ev_levels[value]):
            self._write_register(0x5381 + offset, reg_value)

    @property
    def white_balance(self) -> int:
        """The white balance setting, one of the ``OV5640_WHITE_BALANCE_*`` constants"""
        return self._white_balance

    @white_balance.setter
    def white_balance(self, value: int) -> None:
        if not OV5640_WHITE_BALANCE_AUTO <= value <= OV5640_WHITE_BALANCE_INCANDESCENT:
            raise ValueError(
                "Invalid exposure value (EV) {value}, "
                "use one of the OV5640_WHITE_BALANCE_* constants"
            )
        self._write_register(0x3212, 0x3)  # start group 3
        for reg_addr, reg_value in zip(_light_registers, _light_modes[value]):
            self._write_register(reg_addr, reg_value)
        self._write_register(0x3212, 0x13)  # end group 3
        self._write_register(0x3212, 0xA3)  # launch group 3

    @property
    def night_mode(self) -> bool:
        """Enable or disable the night mode setting of the sensor"""
        return bool(self._read_register(0x3A00) & 0x04)

    @night_mode.setter
    def night_mode(self, value: bool) -> None:
        self._write_reg_bits(0x3A00, 0x04, value)
