/*
 * Copyright (C) 2020 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
 *
 * 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.
 */

#define LOG_TAG "Camera3-RotCropMapper"
#define ATRACE_TAG ATRACE_TAG_CAMERA
//#define LOG_NDEBUG 0

#include <algorithm>
#include <cmath>

#include "device3/RotateAndCropMapper.h"

namespace android {

namespace camera3 {

bool RotateAndCropMapper::isNeeded(const CameraMetadata* deviceInfo) {
    auto entry = deviceInfo->find(ANDROID_SCALER_AVAILABLE_ROTATE_AND_CROP_MODES);
    for (size_t i = 0; i < entry.count; i++) {
        if (entry.data.u8[i] == ANDROID_SCALER_ROTATE_AND_CROP_AUTO) return true;
    }
    return false;
}

RotateAndCropMapper::RotateAndCropMapper(const CameraMetadata* deviceInfo) {
    auto entry = deviceInfo->find(ANDROID_SENSOR_INFO_ACTIVE_ARRAY_SIZE);
    if (entry.count != 4) return;

    mArrayWidth = entry.data.i32[2];
    mArrayHeight = entry.data.i32[3];
    mArrayAspect = static_cast<float>(mArrayWidth) / mArrayHeight;
    mRotateAspect = 1.f/mArrayAspect;
}

/**
 * Adjust capture request when rotate and crop AUTO is enabled
 */
status_t RotateAndCropMapper::updateCaptureRequest(CameraMetadata *request) {
    auto entry = request->find(ANDROID_SCALER_ROTATE_AND_CROP);
    if (entry.count == 0) return OK;
    uint8_t rotateMode = entry.data.u8[0];
    if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_NONE) return OK;

    int32_t cx = 0;
    int32_t cy = 0;
    int32_t cw = mArrayWidth;
    int32_t ch = mArrayHeight;
    entry = request->find(ANDROID_SCALER_CROP_REGION);
    if (entry.count == 4) {
        cx = entry.data.i32[0];
        cy = entry.data.i32[1];
        cw = entry.data.i32[2];
        ch = entry.data.i32[3];
    }

    // User inputs are relative to the rotated-and-cropped view, so convert back
    // to active array coordinates. To be more specific, the application is
    // calculating coordinates based on the crop rectangle and the active array,
    // even though the view the user sees is the cropped-and-rotated one. So we
    // need to adjust the coordinates so that a point that would be on the
    // top-left corner of the crop region is mapped to the top-left corner of
    // the rotated-and-cropped fov within the crop region, and the same for the
    // bottom-right corner.
    //
    // Since the zoom ratio control scales everything uniformly (so an app does
    // not need to adjust anything if it wants to put a metering region on the
    // top-left quadrant of the preview FOV, when changing zoomRatio), it does
    // not need to be factored into this calculation at all.
    //
    //   ->+x                       active array  aw
    //  |+--------------------------------------------------------------------+
    //  v|                                                                    |
    // +y|         a         1       cw        2           b                  |
    //   |          +=========*HHHHHHHHHHHHHHH*===========+                   |
    //   |          I         H      rw       H           I                   |
    //   |          I         H               H           I                   |
    //   |          I         H               H           I                   |
    //ah |       ch I         H rh            H           I crop region       |
    //   |          I         H               H           I                   |
    //   |          I         H               H           I                   |
    //   |          I         H rotate region H           I                   |
    //   |          +=========*HHHHHHHHHHHHHHH*===========+                   |
    //   |         d         4                 3           c                  |
    //   |                                                                    |
    //   +--------------------------------------------------------------------+
    //
    // aw , ah = active array width,height
    // cw , ch = crop region width,height
    // rw , rh = rotated-and-cropped region width,height
    // aw / ah = array aspect = rh / rw = 1 / rotated aspect
    // Coordinate mappings:
    //    ROTATE_AND_CROP_90: point a -> point 2
    //                        point c -> point 4 = +x -> +y, +y -> -x
    //    ROTATE_AND_CROP_180: point a -> point c
    //                         point c -> point a = +x -> -x, +y -> -y
    //    ROTATE_AND_CROP_270: point a -> point 4
    //                         point c -> point 2 = +x -> -y, +y -> +x

    float cropAspect = static_cast<float>(cw) / ch;
    float transformMat[4] = {0, 0,
                             0, 0};
    float xShift = 0;
    float yShift = 0;

    if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_180) {
        transformMat[0] = -1;
        transformMat[3] = -1;
        xShift = cw;
        yShift = ch;
    } else {
        float rw = cropAspect > mRotateAspect ?
                   ch * mRotateAspect : // pillarbox, not full width
                   cw;                  // letterbox or 1:1, full width
        float rh = cropAspect >= mRotateAspect ?
                   ch :                 // pillarbox or 1:1, full height
                   cw / mRotateAspect;  // letterbox, not full height
        switch (rotateMode) {
            case ANDROID_SCALER_ROTATE_AND_CROP_90:
                transformMat[1] = -rw / ch; // +y -> -x
                transformMat[2] =  rh / cw; // +x -> +y
                xShift = (cw + rw) / 2; // left edge of crop to right edge of rotated
                yShift = (ch - rh) / 2; // top edge of crop to top edge of rotated
                break;
            case ANDROID_SCALER_ROTATE_AND_CROP_270:
                transformMat[1] =  rw / ch; // +y -> +x
                transformMat[2] = -rh / cw; // +x -> -y
                xShift = (cw - rw) / 2; // left edge of crop to left edge of rotated
                yShift = (ch + rh) / 2; // top edge of crop to bottom edge of rotated
                break;
            default:
                ALOGE("%s: Unexpected rotate mode: %d", __FUNCTION__, rotateMode);
                return BAD_VALUE;
        }
    }

    for (auto regionTag : kMeteringRegionsToCorrect) {
        entry = request->find(regionTag);
        for (size_t i = 0; i < entry.count; i += 5) {
            int32_t weight = entry.data.i32[i + 4];
            if (weight == 0) {
                continue;
            }
            transformPoints(entry.data.i32 + i, 2, transformMat, xShift, yShift, cx, cy);
            swapRectToMinFirst(entry.data.i32 + i);
        }
    }

    return OK;
}

/**
 * Adjust capture result when rotate and crop AUTO is enabled
 */
status_t RotateAndCropMapper::updateCaptureResult(CameraMetadata *result) {
    auto entry = result->find(ANDROID_SCALER_ROTATE_AND_CROP);
    if (entry.count == 0) return OK;
    uint8_t rotateMode = entry.data.u8[0];
    if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_NONE) return OK;

    int32_t cx = 0;
    int32_t cy = 0;
    int32_t cw = mArrayWidth;
    int32_t ch = mArrayHeight;
    entry = result->find(ANDROID_SCALER_CROP_REGION);
    if (entry.count == 4) {
        cx = entry.data.i32[0];
        cy = entry.data.i32[1];
        cw = entry.data.i32[2];
        ch = entry.data.i32[3];
    }

    // HAL inputs are relative to the full active array, so convert back to
    // rotated-and-cropped coordinates for apps. To be more specific, the
    // application is calculating coordinates based on the crop rectangle and
    // the active array, even though the view the user sees is the
    // cropped-and-rotated one. So we need to adjust the coordinates so that a
    // point that would be on the top-left corner of the rotate-and-cropped
    // region is mapped to the top-left corner of the crop region, and the same
    // for the bottom-right corner.
    //
    // Since the zoom ratio control scales everything uniformly (so an app does
    // not need to adjust anything if it wants to put a metering region on the
    // top-left quadrant of the preview FOV, when changing zoomRatio), it does
    // not need to be factored into this calculation at all.
    //
    // Also note that round-tripping between original request and final result
    // fields can't be perfect, since the intermediate values have to be
    // integers on a smaller range than the original crop region range. That
    // means that multiple input values map to a single output value in
    // adjusting a request, so when adjusting a result, the original answer may
    // not be obtainable.  Given that aspect ratios are rarely > 16/9, the
    // round-trip values should generally only be off by 1 at most.
    //
    //   ->+x                       active array  aw
    //  |+--------------------------------------------------------------------+
    //  v|                                                                    |
    // +y|         a         1       cw        2           b                  |
    //   |          +=========*HHHHHHHHHHHHHHH*===========+                   |
    //   |          I         H      rw       H           I                   |
    //   |          I         H               H           I                   |
    //   |          I         H               H           I                   |
    //ah |       ch I         H rh            H           I crop region       |
    //   |          I         H               H           I                   |
    //   |          I         H               H           I                   |
    //   |          I         H rotate region H           I                   |
    //   |          +=========*HHHHHHHHHHHHHHH*===========+                   |
    //   |         d         4                 3           c                  |
    //   |                                                                    |
    //   +--------------------------------------------------------------------+
    //
    // aw , ah = active array width,height
    // cw , ch = crop region width,height
    // rw , rh = rotated-and-cropped region width,height
    // aw / ah = array aspect = rh / rw = 1 / rotated aspect
    // Coordinate mappings:
    //    ROTATE_AND_CROP_90: point 2 -> point a
    //                        point 4 -> point c = +x -> -y, +y -> +x
    //    ROTATE_AND_CROP_180: point c -> point a
    //                         point a -> point c = +x -> -x, +y -> -y
    //    ROTATE_AND_CROP_270: point 4 -> point a
    //                         point 2 -> point c = +x -> +y, +y -> -x

    float cropAspect = static_cast<float>(cw) / ch;
    float transformMat[4] = {0, 0,
                             0, 0};
    float xShift = 0;
    float yShift = 0;
    float rx = 0; // top-left corner of rotated region
    float ry = 0;
    if (rotateMode == ANDROID_SCALER_ROTATE_AND_CROP_180) {
        transformMat[0] = -1;
        transformMat[3] = -1;
        xShift = cw;
        yShift = ch;
        rx = cx;
        ry = cy;
    } else {
        float rw = cropAspect > mRotateAspect ?
                   ch * mRotateAspect : // pillarbox, not full width
                   cw;                  // letterbox or 1:1, full width
        float rh = cropAspect >= mRotateAspect ?
                   ch :                 // pillarbox or 1:1, full height
                   cw / mRotateAspect;  // letterbox, not full height
        rx = cx + (cw - rw) / 2;
        ry = cy + (ch - rh) / 2;
        switch (rotateMode) {
            case ANDROID_SCALER_ROTATE_AND_CROP_90:
                transformMat[1] =  ch / rw; // +y -> +x
                transformMat[2] = -cw / rh; // +x -> -y
                xShift = -(cw - rw) / 2; // left edge of rotated to left edge of cropped
                yShift = ry - cy + ch;   // top edge of rotated to bottom edge of cropped
                break;
            case ANDROID_SCALER_ROTATE_AND_CROP_270:
                transformMat[1] = -ch / rw; // +y -> -x
                transformMat[2] =  cw / rh; // +x -> +y
                xShift = (cw + rw) / 2; // left edge of rotated to left edge of cropped
                yShift = (ch - rh) / 2; // top edge of rotated to bottom edge of cropped
                break;
            default:
                ALOGE("%s: Unexpected rotate mode: %d", __FUNCTION__, rotateMode);
                return BAD_VALUE;
        }
    }

    for (auto regionTag : kMeteringRegionsToCorrect) {
        entry = result->find(regionTag);
        for (size_t i = 0; i < entry.count; i += 5) {
            int32_t weight = entry.data.i32[i + 4];
            if (weight == 0) {
                continue;
            }
            transformPoints(entry.data.i32 + i, 2, transformMat, xShift, yShift, rx, ry);
            swapRectToMinFirst(entry.data.i32 + i);
        }
    }

    for (auto pointsTag: kResultPointsToCorrectNoClamp) {
        entry = result->find(pointsTag);
        transformPoints(entry.data.i32, entry.count / 2, transformMat, xShift, yShift, rx, ry);
        if (pointsTag == ANDROID_STATISTICS_FACE_RECTANGLES) {
            for (size_t i = 0; i < entry.count; i += 4) {
                swapRectToMinFirst(entry.data.i32 + i);
            }
        }
    }

    return OK;
}

void RotateAndCropMapper::transformPoints(int32_t* pts, size_t count, float transformMat[4],
        float xShift, float yShift, float ox, float oy) {
    for (size_t i = 0; i < count * 2; i += 2) {
        float x0 = pts[i] - ox;
        float y0 = pts[i + 1] - oy;
        int32_t nx = std::round(transformMat[0] * x0 + transformMat[1] * y0 + xShift + ox);
        int32_t ny = std::round(transformMat[2] * x0 + transformMat[3] * y0 + yShift + oy);

        pts[i] = std::min(std::max(nx, 0), mArrayWidth);
        pts[i + 1] = std::min(std::max(ny, 0), mArrayHeight);
    }
}

void RotateAndCropMapper::swapRectToMinFirst(int32_t* rect) {
    if (rect[0] > rect[2]) {
        auto tmp = rect[0];
        rect[0] = rect[2];
        rect[2] = tmp;
    }
    if (rect[1] > rect[3]) {
        auto tmp = rect[1];
        rect[1] = rect[3];
        rect[3] = tmp;
    }
}

} // namespace camera3

} // namespace android
