/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use webrender::ShaderKind;
use webrender_build::shader::{ShaderFeatureFlags, ShaderVersion, build_shader_strings};
use webrender_build::shader::get_shader_features;
use glsl_lang::ast::{InterpolationQualifierData, NodeContent, SingleDeclaration};
use glsl_lang::ast::{StorageQualifierData, TranslationUnit, TypeSpecifierNonArrayData};
use glsl_lang::ast::TypeQualifierSpecData;
use glsl_lang::parse::DefaultParse as _;
use glsl_lang::visitor::{Host, Visit, Visitor};

/// Tests that a shader contains no flat scalar varyings.
/// These must be avoided on Adreno 3xx devices due to bug 1630356.
fn test_no_flat_scalar_varyings(
    name: &str,
    shader: &mut TranslationUnit,
    _shader_kind: ShaderKind,
) {
    struct FlatScalarVaryingsVisitor {
        shader_name: String,
    }

    impl Visitor for FlatScalarVaryingsVisitor {
        fn visit_single_declaration(&mut self, declaration: &SingleDeclaration) -> Visit {
            let is_scalar = matches!(
                declaration.ty.ty.ty.content,
                TypeSpecifierNonArrayData::Bool
                    | TypeSpecifierNonArrayData::Int
                    | TypeSpecifierNonArrayData::UInt
                    | TypeSpecifierNonArrayData::Float
                    | TypeSpecifierNonArrayData::Double
            );

            let qualifiers = declaration
                .ty
                .qualifier
                .as_ref()
                .map(|q| q.qualifiers.as_slice())
                .unwrap_or(&[]);

            let is_flat = qualifiers.contains(
                &TypeQualifierSpecData::Interpolation(InterpolationQualifierData::Flat.into_node())
                    .into_node(),
            );

            assert!(
                !(is_scalar && is_flat),
                "{}: {} is a flat scalar varying",
                self.shader_name,
                &declaration.name.as_ref().unwrap()
            );

            Visit::Parent
        }
    }

    let mut visitor = FlatScalarVaryingsVisitor {
        shader_name: name.to_string(),
    };
    shader.visit(&mut visitor);
}

/// Tests that a shader's varyings have an explicit precision specifier.
/// Mali vendor tooling shows us that we are often varying-iterpolation bound, so using mediump
/// where possible helps alleviate this. By enforcing that varyings are given explicit precisions,
/// we ensure that highp is only used when necessary rather than just by default.
fn test_varying_explicit_precision(
    name: &str,
    shader: &mut TranslationUnit,
    shader_kind: ShaderKind,
) {
    struct VaryingExplicitPrecisionVisitor {
        shader_name: String,
        shader_kind: ShaderKind,
    }

    impl Visitor for VaryingExplicitPrecisionVisitor {
        fn visit_single_declaration(&mut self, declaration: &SingleDeclaration) -> Visit {
            let qualifiers = declaration
                .ty
                .qualifier
                .as_ref()
                .map(|q| q.qualifiers.as_slice())
                .unwrap_or(&[]);

            let is_varying = qualifiers.iter().any(|qualifier| {
                match &qualifier.content {
                    TypeQualifierSpecData::Storage(storage) => match self.shader_kind {
                        ShaderKind::Vertex => storage.content == StorageQualifierData::Out,
                        ShaderKind::Fragment => storage.content == StorageQualifierData::In,
                    }
                    _ => false,
                }
            });

            let has_explicit_precision = qualifiers
                .iter()
                .any(|qualifier| matches!(qualifier.content, TypeQualifierSpecData::Precision(_)));

            assert!(
                !is_varying || has_explicit_precision,
                "{}: {} is a varying without an explicit precision declared",
                self.shader_name,
                &declaration.name.as_ref().unwrap()
            );

            Visit::Parent
        }
    }

    let mut visitor = VaryingExplicitPrecisionVisitor {
        shader_name: name.to_string(),
        shader_kind,
    };
    shader.visit(&mut visitor);
}

pub fn test_shaders() {
    let mut flags = ShaderFeatureFlags::all();
    if cfg!(any(target_os = "windows", target_os = "android")) {
        flags.remove(ShaderFeatureFlags::GL);
    } else {
        flags.remove(ShaderFeatureFlags::GLES);
    }
    // glsl-lang crate fails to parse advanced blend shaders
    flags.remove(ShaderFeatureFlags::ADVANCED_BLEND_EQUATION);

    for (shader, configs) in get_shader_features(flags) {
        for config in configs {
            let name = if config.is_empty() {
                shader.to_string()
            } else {
                format!("{}_{}", shader, config.replace(",", "_"))
            };
            let vert_name = format!("{}.vert", name);
            let frag_name = format!("{}.frag", name);


            let features = config
                .split(",")
                .filter(|f| !f.is_empty())
                .collect::<Vec<_>>();

            let (vert_src, frag_src) =
                build_shader_strings(ShaderVersion::Gles, &features, shader, &|f| {
                    webrender::get_unoptimized_shader_source(f, None)
                });

            let mut vert = TranslationUnit::parse(&vert_src).unwrap();
            let mut frag = TranslationUnit::parse(&frag_src).unwrap();


            test_no_flat_scalar_varyings(&vert_name, &mut vert, ShaderKind::Vertex);
            test_no_flat_scalar_varyings(&frag_name, &mut frag, ShaderKind::Fragment);
            test_varying_explicit_precision(&vert_name, &mut vert, ShaderKind::Vertex);
            test_varying_explicit_precision(&frag_name, &mut frag, ShaderKind::Fragment);
        }
    }
}
