#!/usr/bin/env python3

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (C) 2022, Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>

# A simple libcamera capture example
#
# This is a python version of simple-cam from:
# https://git.libcamera.org/libcamera/simple-cam.git
#
# \todo Move to simple-cam repository when the Python API has stabilized more

import libcamera as libcam
import selectors
import sys
import time

TIMEOUT_SEC = 3


def handle_camera_event(cm):
    # cm.get_ready_requests() returns the ready requests, which in our case
    # should almost always return a single Request, but in some cases there
    # could be multiple or none.

    reqs = cm.get_ready_requests()

    # Process the captured frames

    for req in reqs:
        process_request(req)


def process_request(request):
    global camera

    print()

    print(f'Request completed: {request}')

    # When a request has completed, it is populated with a metadata control
    # list that allows an application to determine various properties of
    # the completed request. This can include the timestamp of the Sensor
    # capture, or its gain and exposure values, or properties from the IPA
    # such as the state of the 3A algorithms.
    #
    # To examine each request, print all the metadata for inspection. A custom
    # application can parse each of these items and process them according to
    # its needs.

    requestMetadata = request.metadata
    for id, value in requestMetadata.items():
        print(f'\t{id.name} = {value}')

    # Each buffer has its own FrameMetadata to describe its state, or the
    # usage of each buffer. While in our simple capture we only provide one
    # buffer per request, a request can have a buffer for each stream that
    # is established when configuring the camera.
    #
    # This allows a viewfinder and a still image to be processed at the
    # same time, or to allow obtaining the RAW capture buffer from the
    # sensor along with the image as processed by the ISP.

    buffers = request.buffers
    for _, buffer in buffers.items():
        metadata = buffer.metadata

        # Print some information about the buffer which has completed.
        print(f' seq: {metadata.sequence:06} timestamp: {metadata.timestamp} bytesused: ' +
              '/'.join([str(p.bytes_used) for p in metadata.planes]))

        # Image data can be accessed here, but the FrameBuffer
        # must be mapped by the application

    # Re-queue the Request to the camera.
    request.reuse()
    camera.queue_request(request)


# ----------------------------------------------------------------------------
# Camera Naming.
#
# Applications are responsible for deciding how to name cameras, and present
# that information to the users. Every camera has a unique identifier, though
# this string is not designed to be friendly for a human reader.
#
# To support human consumable names, libcamera provides camera properties
# that allow an application to determine a naming scheme based on its needs.
#
# In this example, we focus on the location property, but also detail the
# model string for external cameras, as this is more likely to be visible
# information to the user of an externally connected device.
#
# The unique camera ID is appended for informative purposes.
#
def camera_name(camera):
    props = camera.properties
    location = props.get(libcam.properties.Location, None)

    if location == libcam.properties.LocationEnum.Front:
        name = 'Internal front camera'
    elif location == libcam.properties.LocationEnum.Back:
        name = 'Internal back camera'
    elif location == libcam.properties.LocationEnum.External:
        name = 'External camera'
        if libcam.properties.Model in props:
            name += f' "{props[libcam.properties.Model]}"'
    else:
        name = 'Undefined location'

    name += f' ({camera.id})'

    return name


def main():
    global camera

    # --------------------------------------------------------------------
    # Get the Camera Manager.
    #
    # The Camera Manager is responsible for enumerating all the Camera
    # in the system, by associating Pipeline Handlers with media entities
    # registered in the system.
    #
    # The CameraManager provides a list of available Cameras that
    # applications can operate on.
    #
    # There can only be a single CameraManager within any process space.

    cm = libcam.CameraManager.singleton()

    # Just as a test, generate names of the Cameras registered in the
    # system, and list them.

    for camera in cm.cameras:
        print(f' - {camera_name(camera)}')

    # --------------------------------------------------------------------
    # Camera
    #
    # Camera are entities created by pipeline handlers, inspecting the
    # entities registered in the system and reported to applications
    # by the CameraManager.
    #
    # In general terms, a Camera corresponds to a single image source
    # available in the system, such as an image sensor.
    #
    # Application lock usage of Camera by 'acquiring' them.
    # Once done with it, application shall similarly 'release' the Camera.
    #
    # As an example, use the first available camera in the system after
    # making sure that at least one camera is available.
    #
    # Cameras can be obtained by their ID or their index, to demonstrate
    # this, the following code gets the ID of the first camera; then gets
    # the camera associated with that ID (which is of course the same as
    # cm.cameras[0]).

    if not cm.cameras:
        print('No cameras were identified on the system.')
        return -1

    camera_id = cm.cameras[0].id
    camera = cm.get(camera_id)
    camera.acquire()

    # --------------------------------------------------------------------
    # Stream
    #
    # Each Camera supports a variable number of Stream. A Stream is
    # produced by processing data produced by an image source, usually
    # by an ISP.
    #
    #   +-------------------------------------------------------+
    #   | Camera                                                |
    #   |                +-----------+                          |
    #   | +--------+     |           |------> [  Main output  ] |
    #   | | Image  |     |           |                          |
    #   | |        |---->|    ISP    |------> [   Viewfinder  ] |
    #   | | Source |     |           |                          |
    #   | +--------+     |           |------> [ Still Capture ] |
    #   |                +-----------+                          |
    #   +-------------------------------------------------------+
    #
    # The number and capabilities of the Stream in a Camera are
    # a platform dependent property, and it's the pipeline handler
    # implementation that has the responsibility of correctly
    # report them.

    # --------------------------------------------------------------------
    # Camera Configuration.
    #
    # Camera configuration is tricky! It boils down to assign resources
    # of the system (such as DMA engines, scalers, format converters) to
    # the different image streams an application has requested.
    #
    # Depending on the system characteristics, some combinations of
    # sizes, formats and stream usages might or might not be possible.
    #
    # A Camera produces a CameraConfigration based on a set of intended
    # roles for each Stream the application requires.

    config = camera.generate_configuration([libcam.StreamRole.Viewfinder])

    # The CameraConfiguration contains a StreamConfiguration instance
    # for each StreamRole requested by the application, provided
    # the Camera can support all of them.
    #
    # Each StreamConfiguration has default size and format, assigned
    # by the Camera depending on the Role the application has requested.

    stream_config = config.at(0)
    print(f'Default viewfinder configuration is: {stream_config}')

    # Each StreamConfiguration parameter which is part of a
    # CameraConfiguration can be independently modified by the
    # application.
    #
    # In order to validate the modified parameter, the CameraConfiguration
    # should be validated -before- the CameraConfiguration gets applied
    # to the Camera.
    #
    # The CameraConfiguration validation process adjusts each
    # StreamConfiguration to a valid value.

    # Validating a CameraConfiguration -before- applying it will adjust it
    # to a valid configuration which is as close as possible to the one
    # requested.

    config.validate()
    print(f'Validated viewfinder configuration is: {stream_config}')

    # Once we have a validated configuration, we can apply it to the
    # Camera.

    camera.configure(config)

    # --------------------------------------------------------------------
    # Buffer Allocation
    #
    # Now that a camera has been configured, it knows all about its
    # Streams sizes and formats. The captured images need to be stored in
    # framebuffers which can either be provided by the application to the
    # library, or allocated in the Camera and exposed to the application
    # by libcamera.
    #
    # An application may decide to allocate framebuffers from elsewhere,
    # for example in memory allocated by the display driver that will
    # render the captured frames. The application will provide them to
    # libcamera by constructing FrameBuffer instances to capture images
    # directly into.
    #
    # Alternatively libcamera can help the application by exporting
    # buffers allocated in the Camera using a FrameBufferAllocator
    # instance and referencing a configured Camera to determine the
    # appropriate buffer size and types to create.

    allocator = libcam.FrameBufferAllocator(camera)

    for cfg in config:
        allocated = allocator.allocate(cfg.stream)
        print(f'Allocated {allocated} buffers for stream')

    # --------------------------------------------------------------------
    # Frame Capture
    #
    # libcamera frames capture model is based on the 'Request' concept.
    # For each frame a Request has to be queued to the Camera.
    #
    # A Request refers to (at least one) Stream for which a Buffer that
    # will be filled with image data shall be added to the Request.
    #
    # A Request is associated with a list of Controls, which are tunable
    # parameters (similar to v4l2_controls) that have to be applied to
    # the image.
    #
    # Once a request completes, all its buffers will contain image data
    # that applications can access and for each of them a list of metadata
    # properties that reports the capture parameters applied to the image.

    stream = stream_config.stream
    buffers = allocator.buffers(stream)
    requests = []
    for i in range(len(buffers)):
        request = camera.create_request()

        buffer = buffers[i]
        request.add_buffer(stream, buffer)

        # Controls can be added to a request on a per frame basis.
        request.set_control(libcam.controls.Brightness, 0.5)

        requests.append(request)

    # --------------------------------------------------------------------
    # Start Capture
    #
    # In order to capture frames the Camera has to be started and
    # Request queued to it. Enough Request to fill the Camera pipeline
    # depth have to be queued before the Camera start delivering frames.
    #
    # When a Request has been completed, it will be added to a list in the
    # CameraManager and an event will be raised using eventfd.
    #
    # The list of completed Requests can be retrieved with
    # CameraManager.get_ready_requests(), which will also clear the list in the
    # CameraManager.
    #
    # The eventfd can be retrieved from CameraManager.event_fd, and the fd can
    # be waited upon using e.g. Python's selectors.

    camera.start()
    for request in requests:
        camera.queue_request(request)

    sel = selectors.DefaultSelector()
    sel.register(cm.event_fd, selectors.EVENT_READ, lambda fd: handle_camera_event(cm))

    start_time = time.time()

    while time.time() - start_time < TIMEOUT_SEC:
        events = sel.select()
        for key, mask in events:
            key.data(key.fileobj)

    # --------------------------------------------------------------------
    # Clean Up
    #
    # Stop the Camera, release resources and stop the CameraManager.
    # libcamera has now released all resources it owned.

    camera.stop()
    camera.release()

    return 0


if __name__ == '__main__':
    sys.exit(main())
