mod exiftool;
mod ffmpeg;
mod magick;

use crate::{
    discover::Discovery,
    error::Error,
    error_code::ErrorCode,
    formats::{
        AnimationFormat, AnimationOutput, ImageInput, ImageOutput, InputFile, InputVideoFormat,
        InternalFormat, Validations,
    },
    read::BoxRead,
    tmp_file::TmpDir,
};
use actix_web::web::Bytes;

#[derive(Debug, thiserror::Error)]
pub(crate) enum ValidationError {
    #[error("Too wide")]
    Width,

    #[error("Too tall")]
    Height,

    #[error("Too many pixels")]
    Area,

    #[error("Too many frames")]
    Frames,

    #[error("Uploaded file is empty")]
    Empty,

    #[error("Filesize too large")]
    Filesize,

    #[error("Video is disabled")]
    VideoDisabled,
}

impl ValidationError {
    pub(crate) const fn error_code(&self) -> ErrorCode {
        match self {
            Self::Width => ErrorCode::VALIDATE_WIDTH,
            Self::Height => ErrorCode::VALIDATE_HEIGHT,
            Self::Area => ErrorCode::VALIDATE_AREA,
            Self::Frames => ErrorCode::VALIDATE_FRAMES,
            Self::Empty => ErrorCode::VALIDATE_FILE_EMPTY,
            Self::Filesize => ErrorCode::VALIDATE_FILE_SIZE,
            Self::VideoDisabled => ErrorCode::VIDEO_DISABLED,
        }
    }
}

const MEGABYTES: usize = 1024 * 1024;

#[tracing::instrument(skip_all)]
pub(crate) async fn validate_bytes(
    tmp_dir: &TmpDir,
    bytes: Bytes,
    validations: Validations<'_>,
    timeout: u64,
) -> Result<(InternalFormat, BoxRead<'static>), Error> {
    if bytes.is_empty() {
        return Err(ValidationError::Empty.into());
    }

    let Discovery {
        input,
        width,
        height,
        frames,
    } = crate::discover::discover_bytes(tmp_dir, timeout, bytes.clone()).await?;

    match &input {
        InputFile::Image(input) => {
            let (format, read) = process_image(
                tmp_dir,
                bytes,
                *input,
                width,
                height,
                validations.image,
                timeout,
            )
            .await?;

            Ok((format, read))
        }
        InputFile::Animation(input) => {
            let (format, read) = process_animation(
                tmp_dir,
                bytes,
                *input,
                width,
                height,
                frames.unwrap_or(1),
                validations.animation,
                timeout,
            )
            .await?;

            Ok((format, read))
        }
        InputFile::Video(input) => {
            let (format, read) = process_video(
                tmp_dir,
                bytes,
                *input,
                width,
                height,
                frames.unwrap_or(1),
                validations.video,
                timeout,
            )
            .await?;

            Ok((format, read))
        }
    }
}

#[tracing::instrument(skip(bytes, validations))]
async fn process_image(
    tmp_dir: &TmpDir,
    bytes: Bytes,
    input: ImageInput,
    width: u16,
    height: u16,
    validations: &crate::config::Image,
    timeout: u64,
) -> Result<(InternalFormat, BoxRead<'static>), Error> {
    if width > validations.max_width {
        return Err(ValidationError::Width.into());
    }
    if height > validations.max_height {
        return Err(ValidationError::Height.into());
    }
    if u32::from(width) * u32::from(height) > validations.max_area {
        return Err(ValidationError::Area.into());
    }
    if bytes.len() > validations.max_file_size * MEGABYTES {
        return Err(ValidationError::Filesize.into());
    }

    let ImageOutput {
        format,
        needs_transcode,
    } = input.build_output(validations.format);

    let read = if needs_transcode {
        let quality = validations.quality_for(format);

        magick::convert_image(tmp_dir, input.format, format, quality, timeout, bytes).await?
    } else {
        exiftool::clear_metadata_bytes_read(bytes, timeout)?
    };

    Ok((InternalFormat::Image(format), read))
}

fn validate_animation(
    size: usize,
    width: u16,
    height: u16,
    frames: u32,
    validations: &crate::config::Animation,
) -> Result<(), ValidationError> {
    if width > validations.max_width {
        return Err(ValidationError::Width);
    }
    if height > validations.max_height {
        return Err(ValidationError::Height);
    }
    if u32::from(width) * u32::from(height) > validations.max_area {
        return Err(ValidationError::Area);
    }
    if frames > validations.max_frame_count {
        return Err(ValidationError::Frames);
    }
    if size > validations.max_file_size * MEGABYTES {
        return Err(ValidationError::Filesize);
    }

    Ok(())
}

#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(tmp_dir, bytes, validations))]
async fn process_animation(
    tmp_dir: &TmpDir,
    bytes: Bytes,
    input: AnimationFormat,
    width: u16,
    height: u16,
    frames: u32,
    validations: &crate::config::Animation,
    timeout: u64,
) -> Result<(InternalFormat, BoxRead<'static>), Error> {
    validate_animation(bytes.len(), width, height, frames, validations)?;

    let AnimationOutput {
        format,
        needs_transcode,
    } = input.build_output(validations.format);

    let read = if needs_transcode {
        let quality = validations.quality_for(format);

        magick::convert_animation(tmp_dir, input, format, quality, timeout, bytes).await?
    } else {
        exiftool::clear_metadata_bytes_read(bytes, timeout)?
    };

    Ok((InternalFormat::Animation(format), read))
}

fn validate_video(
    size: usize,
    width: u16,
    height: u16,
    frames: u32,
    validations: &crate::config::Video,
) -> Result<(), ValidationError> {
    if !validations.enable {
        return Err(ValidationError::VideoDisabled);
    }
    if width > validations.max_width {
        return Err(ValidationError::Width);
    }
    if height > validations.max_height {
        return Err(ValidationError::Height);
    }
    if u32::from(width) * u32::from(height) > validations.max_area {
        return Err(ValidationError::Area);
    }
    if frames > validations.max_frame_count {
        return Err(ValidationError::Frames);
    }
    if size > validations.max_file_size * MEGABYTES {
        return Err(ValidationError::Filesize);
    }

    Ok(())
}

#[allow(clippy::too_many_arguments)]
#[tracing::instrument(skip(tmp_dir, bytes, validations))]
async fn process_video(
    tmp_dir: &TmpDir,
    bytes: Bytes,
    input: InputVideoFormat,
    width: u16,
    height: u16,
    frames: u32,
    validations: &crate::config::Video,
    timeout: u64,
) -> Result<(InternalFormat, BoxRead<'static>), Error> {
    validate_video(bytes.len(), width, height, frames, validations)?;

    let output = input.build_output(
        validations.video_codec,
        validations.audio_codec,
        validations.allow_audio,
    );

    let crf = validations.crf_for(width, height);

    let read = ffmpeg::transcode_bytes(tmp_dir, input, output, crf, timeout, bytes).await?;

    Ok((InternalFormat::Video(output.format.internal_format()), read))
}
