"""Inspect fixed regions in a stationary shot; flags require visual review."""
import argparse
import csv
import json
from pathlib import Path

import imageio_ffmpeg
import numpy as np


def rectangle(value):
    try:
        x, y, width, height = map(int, value.split(','))
    except ValueError as error:
        raise argparse.ArgumentTypeError('Use x,y,width,height in source pixels') from error
    if min(x, y) < 0 or min(width, height) <= 0:
        raise argparse.ArgumentTypeError('Coordinates must be nonnegative and sizes positive')
    return x, y, width, height


def region(frame, box):
    x, y, width, height = box
    if x + width > frame.shape[1] or y + height > frame.shape[0]:
        raise ValueError('Region extends beyond the decoded frame')
    return frame[y:y + height, x:x + width].astype(float)


def measure(filename, roi, background, limit=300, residual_threshold=4.0):
    reader = imageio_ffmpeg.read_frames(str(filename), pix_fmt='rgb24')
    metadata = next(reader)
    width, height = metadata['size']
    if metadata['fps'] <= 0:
        reader.close()
        raise ValueError('The decoder did not report a usable frame rate')
    if width * height > 3840 * 2160:
        reader.close()
        raise ValueError('Use a clip no larger than 3840x2160 pixels')
    previous = None
    rows = []
    try:
        for index, raw in enumerate(reader):
            if index >= limit:
                break
            frame = np.frombuffer(raw, np.uint8).reshape(height, width, 3)
            current_bg = region(frame, background).mean()
            current_roi = region(frame, roi)
            if current_bg < 8:
                raise ValueError('Background is too dark for a stable brightness ratio')
            if previous is not None:
                previous_bg, previous_roi = previous
                ratio = current_bg / previous_bg
                residual = float(np.abs(current_roi - previous_roi * ratio).mean())
                rows.append({'frame': index, 'time_seconds': index / metadata['fps'],
                             'background_ratio': float(ratio), 'residual_rgb': residual,
                             'lighting_flag': bool(abs(ratio - 1) > 0.02),
                             'detail_flag': bool(residual > residual_threshold),
                             'review_pair': bool(abs(ratio - 1) > 0.02 or residual > residual_threshold)})
            previous = current_bg, current_roi
    finally:
        reader.close()
    if not rows:
        raise ValueError('At least two decoded frames are required')
    summary = {'pairs': len(rows), 'mean_abs_ratio_minus_one': float(np.mean([
        abs(row['background_ratio'] - 1) for row in rows])),
        'mean_residual_rgb': float(np.mean([row['residual_rgb'] for row in rows])),
        'flagged_pairs': sum(row['review_pair'] for row in rows)}
    return rows, summary


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--input', type=Path, required=True)
    parser.add_argument('--roi', type=rectangle, required=True, help='Fixed product region x,y,w,h')
    parser.add_argument('--bg', type=rectangle, required=True, help='Fixed background region x,y,w,h')
    parser.add_argument('--output', type=Path, required=True, help='New CSV file; never overwrites')
    parser.add_argument('--max-frames', type=int, default=300)
    parser.add_argument('--residual-threshold', type=float, default=4.0,
                        help='Review threshold calibrated on the supplied controls, not a universal quality limit')
    args = parser.parse_args()
    if not 2 <= args.max_frames <= 3000:
        parser.error('--max-frames must be between 2 and 3000')
    if not 0 < args.residual_threshold <= 255:
        parser.error('--residual-threshold must be greater than zero and at most 255')
    if args.output.exists():
        parser.error('Output already exists; choose a new CSV filename')
    try:
        rows, summary = measure(args.input, args.roi, args.bg, args.max_frames, args.residual_threshold)
        args.output.parent.mkdir(parents=True, exist_ok=True)
        with args.output.open('x', newline='', encoding='utf8') as stream:
            writer = csv.DictWriter(stream, fieldnames=rows[0].keys())
            writer.writeheader()
            writer.writerows(rows)
    except (ValueError, OSError, RuntimeError) as error:
        parser.exit(1, f'Could not inspect clip: {error}\n')
    print(json.dumps(summary, indent=2))
    print('Review flagged pairs against the original reference. Motion, shadows and reflections can trigger flags.')


if __name__ == '__main__':
    main()
