﻿using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace HorizonBasedAmbientOcclusion.Universal
{
    [ExecuteInEditMode, VolumeComponentMenu("Lighting/HBAO")]
    public class HBAO : VolumeComponent, IPostProcessComponent
    {
        public enum Preset
        {
            FastestPerformance,
            FastPerformance,
            Normal,
            HighQuality,
            HighestQuality,
            Custom
        }

        public enum Mode
        {
            Normal,
            LitAO
        }

        public enum RenderingPath
        {
            Forward,
            Deferred
        }

        public enum Quality
        {
            Lowest,
            Low,
            Medium,
            High,
            Highest
        }

        public enum Resolution
        {
            Full,
            Half
        }

        public enum NoiseType
        {
            Dither,
            InterleavedGradientNoise,
            SpatialDistribution
        }

        public enum Deinterleaving
        {
            Disabled,
            x4
        }

        public enum DebugMode
        {
            Disabled,
            AOOnly,
            ColorBleedingOnly,
            SplitWithoutAOAndWithAO,
            SplitWithAOAndAOOnly,
            SplitWithoutAOAndAOOnly,
            ViewNormals
        }

        public enum BlurType
        {
            None,
            Narrow,
            Medium,
            Wide,
            ExtraWide
        }

        public enum PerPixelNormals
        {
            Reconstruct2Samples,
            Reconstruct4Samples,
            Camera
        }

        public enum VarianceClipping
        {
            Disabled,
            _4Tap,
            _8Tap
        }

        [Serializable]
        public sealed class PresetParameter : VolumeParameter<Preset>
        {
            public PresetParameter(Preset value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class ModeParameter : VolumeParameter<Mode>
        {
            public ModeParameter(Mode value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class RenderingPathParameter : VolumeParameter<RenderingPath>
        {
            public RenderingPathParameter(RenderingPath value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class QualityParameter : VolumeParameter<Quality>
        {
            public QualityParameter(Quality value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class DeinterleavingParameter : VolumeParameter<Deinterleaving>
        {
            public DeinterleavingParameter(Deinterleaving value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class ResolutionParameter : VolumeParameter<Resolution>
        {
            public ResolutionParameter(Resolution value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class NoiseTypeParameter : VolumeParameter<NoiseType>
        {
            public NoiseTypeParameter(NoiseType value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class DebugModeParameter : VolumeParameter<DebugMode>
        {
            public DebugModeParameter(DebugMode value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class PerPixelNormalsParameter : VolumeParameter<PerPixelNormals>
        {
            public PerPixelNormalsParameter(PerPixelNormals value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class VarianceClippingParameter : VolumeParameter<VarianceClipping>
        {
            public VarianceClippingParameter(VarianceClipping value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class BlurTypeParameter : VolumeParameter<BlurType>
        {
            public BlurTypeParameter(BlurType value, bool overrideState = false)
                : base(value, overrideState) { }
        }

        [Serializable]
        public sealed class MinMaxFloatParameter : VolumeParameter<Vector2>
        {
            public float min;
            public float max;

            public MinMaxFloatParameter(Vector2 value, float min, float max, bool overrideState = false)
                : base(value, overrideState)
            {
                this.min = min;
                this.max = max;
            }
        }

        [AttributeUsage(AttributeTargets.Field)]
        public class SettingsGroup : Attribute
        {
            public bool isExpanded = true;
        }

        [AttributeUsage(AttributeTargets.Field)]
        public class ParameterDisplayName : Attribute
        {
            public string name;

            public ParameterDisplayName(string name)
            {
                this.name = name;
            }
        }

        public class Presets : SettingsGroup { }
        public class GeneralSettings : SettingsGroup { }
        public class AOSettings : SettingsGroup { }
        public class TemporalFilterSettings : SettingsGroup { }
        public class BlurSettings : SettingsGroup { }
        public class ColorBleedingSettings : SettingsGroup { }

        [Presets]
        public PresetParameter preset = new PresetParameter(Preset.Normal);

        [Tooltip("The mode of the AO.")]
        [GeneralSettings, Space(6)]
        public ModeParameter mode = new ModeParameter(Mode.LitAO);
        [Tooltip("The rendering path used for AO. Temporary settings as for now rendering path is internal to renderer settings.")]
        [GeneralSettings, Space(6)]
        public RenderingPathParameter renderingPath = new RenderingPathParameter(RenderingPath.Forward);
        [Tooltip("The quality of the AO.")]
        [GeneralSettings, Space(6)]
        public QualityParameter quality = new QualityParameter(Quality.Medium);
        [Tooltip("The deinterleaving factor.")]
        [GeneralSettings]
        public DeinterleavingParameter deinterleaving = new DeinterleavingParameter(Deinterleaving.Disabled);
        [Tooltip("The resolution at which the AO is calculated.")]
        [GeneralSettings]
        public ResolutionParameter resolution = new ResolutionParameter(Resolution.Full);
        [Tooltip("The type of noise to use.")]
        [GeneralSettings, Space(10)]
        public NoiseTypeParameter noiseType = new NoiseTypeParameter(NoiseType.Dither);
        [Tooltip("The debug mode actually displayed on screen.")]
        [GeneralSettings, Space(10)]
        public DebugModeParameter debugMode = new DebugModeParameter(DebugMode.Disabled);

        [Tooltip("AO radius: this is the distance outside which occluders are ignored.")]
        [AOSettings, Space(6)]
        public ClampedFloatParameter radius = new ClampedFloatParameter(0.8f, 0.25f, 5f);
        [Tooltip("Maximum radius in pixels: this prevents the radius to grow too much with close-up " +
                  "object and impact on performances.")]
        [AOSettings]
        public ClampedFloatParameter maxRadiusPixels = new ClampedFloatParameter(128f, 16f, 256f);
        [Tooltip("For low-tessellated geometry, occlusion variations tend to appear at creases and " +
                 "ridges, which betray the underlying tessellation. To remove these artifacts, we use " +
                 "an angle bias parameter which restricts the hemisphere.")]
        [AOSettings]
        public ClampedFloatParameter bias = new ClampedFloatParameter(0.05f, 0f, 0.5f);
        [Tooltip("This value allows to scale up the ambient occlusion values.")]
        [AOSettings]
        public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0, 4f);
        [Tooltip("Enable/disable MultiBounce approximation.")]
        [AOSettings]
        public BoolParameter useMultiBounce = new BoolParameter(false);
        [Tooltip("MultiBounce approximation influence.")]
        [AOSettings]
        public ClampedFloatParameter multiBounceInfluence = new ClampedFloatParameter(1f, 0f, 1f);
        [Tooltip("How much AO affect direct lighting.")]
        [AOSettings]
        public ClampedFloatParameter directLightingStrength = new ClampedFloatParameter(0.25f, 0, 1f);
        [Tooltip("The amount of AO offscreen samples are contributing.")]
        [AOSettings]
        public ClampedFloatParameter offscreenSamplesContribution = new ClampedFloatParameter(0f, 0f, 1f);
        [Tooltip("The max distance to display AO.")]
        [AOSettings, Space(10)]
        public FloatParameter maxDistance = new FloatParameter(150f);
        [Tooltip("The distance before max distance at which AO start to decrease.")]
        [AOSettings]
        public FloatParameter distanceFalloff = new FloatParameter(50f);
        [Tooltip("The type of per pixel normals to use.")]
        [AOSettings, Space(10)]
        public PerPixelNormalsParameter perPixelNormals = new PerPixelNormalsParameter(PerPixelNormals.Camera);
        [Tooltip("This setting allow you to set the base color if the AO, the alpha channel value is unused.")]
        [AOSettings, Space(10)]
        public ColorParameter baseColor = new ColorParameter(Color.black);

        [TemporalFilterSettings, ParameterDisplayName("Enabled"), Space(6)]
        public BoolParameter temporalFilterEnabled = new BoolParameter(false);
        [Tooltip("The type of variance clipping to use.")]
        [TemporalFilterSettings]
        public VarianceClippingParameter varianceClipping = new VarianceClippingParameter(VarianceClipping._4Tap);

        [Tooltip("The type of blur to use.")]
        [BlurSettings, ParameterDisplayName("Type"), Space(6)]
        public BlurTypeParameter blurType = new BlurTypeParameter(BlurType.Medium);

        [Tooltip("This parameter controls the depth-dependent weight of the bilateral filter, to " +
                 "avoid bleeding across edges. A zero sharpness is a pure Gaussian blur. Increasing " +
                 "the blur sharpness removes bleeding by using lower weights for samples with large " +
                 "depth delta from the current pixel.")]
        [BlurSettings, Space(10)]
        public ClampedFloatParameter sharpness = new ClampedFloatParameter(8f, 0f, 16f);

        [ColorBleedingSettings, ParameterDisplayName("Enabled"), Space(6)]
        public BoolParameter colorBleedingEnabled = new BoolParameter(false);
        [Tooltip("This value allows to control the saturation of the color bleeding.")]
        [ColorBleedingSettings, Space(10)]
        public ClampedFloatParameter saturation = new ClampedFloatParameter(1f, 0f, 4f);
        [Tooltip("Use masking on emissive pixels")]
        [ColorBleedingSettings]
        public ClampedFloatParameter brightnessMask = new ClampedFloatParameter(1f, 0f, 1f);
        [Tooltip("Brightness level where masking starts/ends")]
        [ColorBleedingSettings]
        public MinMaxFloatParameter brightnessMaskRange = new MinMaxFloatParameter(new Vector2(0f, 0.5f), 0f, 2f);

        public void EnableHBAO(bool enable)
        {
            intensity.overrideState = enable;
        }

        public Preset GetCurrentPreset()
        {
            return preset.value;
        }

        public void ApplyPreset(Preset preset)
        {
            if (preset == Preset.Custom)
            {
                this.preset.Override(preset);
                return;
            }

            var actualDebugMode = debugMode.value;
            var actualDebugModeOverride = debugMode.overrideState;
            SetAllOverridesTo(false);
            debugMode.overrideState = actualDebugModeOverride;
            debugMode.value = actualDebugMode;

            switch (preset)
            {
                case Preset.FastestPerformance:
                    SetQuality(Quality.Lowest);
                    SetAoRadius(0.5f);
                    SetAoMaxRadiusPixels(64.0f);
                    SetBlurType(BlurType.ExtraWide);
                    break;
                case Preset.FastPerformance:
                    SetQuality(Quality.Low);
                    SetAoRadius(0.5f);
                    SetAoMaxRadiusPixels(64.0f);
                    SetBlurType(BlurType.Wide);
                    break;
                case Preset.HighQuality:
                    SetQuality(Quality.High);
                    SetAoRadius(1.0f);
                    break;
                case Preset.HighestQuality:
                    SetQuality(Quality.Highest);
                    SetAoRadius(1.2f);
                    SetAoMaxRadiusPixels(256.0f);
                    SetBlurType(BlurType.Narrow);
                    break;
                case Preset.Normal:
                default:
                    break;
            }

            this.preset.Override(preset);
        }

        public Mode GetMode()
        {
            return mode.value;
        }

        public void SetMode(Mode mode)
        {
            this.mode.Override(mode);
        }

        public RenderingPath GetRenderingPath()
        {
            return renderingPath.value;
        }

        public void SetRenderingPath(RenderingPath renderingPath)
        {
            this.renderingPath.Override(renderingPath);
        }

        public Quality GetQuality()
        {
            return quality.value;
        }

        public void SetQuality(Quality quality)
        {
            this.quality.Override(quality);
        }

        public Deinterleaving GetDeinterleaving()
        {
            return deinterleaving.value;
        }

        public void SetDeinterleaving(Deinterleaving deinterleaving)
        {
            this.deinterleaving.Override(deinterleaving);
        }

        public Resolution GetResolution()
        {
            return resolution.value;
        }

        public void SetResolution(Resolution resolution)
        {
            this.resolution.Override(resolution);
        }

        public NoiseType GetNoiseType()
        {
            return noiseType.value;
        }

        public void SetNoiseType(NoiseType noiseType)
        {
            this.noiseType.Override(noiseType);
        }

        public DebugMode GetDebugMode()
        {
            return debugMode.value;
        }

        public void SetDebugMode(DebugMode debugMode)
        {
            this.debugMode.Override(debugMode);
        }

        public float GetAoRadius()
        {
            return radius.value;
        }

        public void SetAoRadius(float radius)
        {
            this.radius.Override(Mathf.Clamp(radius, this.radius.min, this.radius.max));
        }

        public float GetAoMaxRadiusPixels()
        {
            return maxRadiusPixels.value;
        }

        public void SetAoMaxRadiusPixels(float maxRadiusPixels)
        {
            this.maxRadiusPixels.Override(Mathf.Clamp(maxRadiusPixels, this.maxRadiusPixels.min, this.maxRadiusPixels.max));
        }

        public float GetAoBias()
        {
            return bias.value;
        }

        public void SetAoBias(float bias)
        {
            this.bias.Override(Mathf.Clamp(bias, this.bias.min, this.bias.max));
        }

        public float GetAoOffscreenSamplesContribution()
        {
            return offscreenSamplesContribution.value;
        }

        public void SetAoOffscreenSamplesContribution(float offscreenSamplesContribution)
        {
            this.offscreenSamplesContribution.Override(Mathf.Clamp(offscreenSamplesContribution, this.offscreenSamplesContribution.min, this.offscreenSamplesContribution.max));
        }

        public float GetAoMaxDistance()
        {
            return maxDistance.value;
        }

        public void SetAoMaxDistance(float maxDistance)
        {
            this.maxDistance.Override(maxDistance);
        }

        public float GetAoDistanceFalloff()
        {
            return distanceFalloff.value;
        }

        public void SetAoDistanceFalloff(float distanceFalloff)
        {
            this.distanceFalloff.Override(distanceFalloff);
        }

        public PerPixelNormals GetAoPerPixelNormals()
        {
            return perPixelNormals.value;
        }

        public void SetAoPerPixelNormals(PerPixelNormals perPixelNormals)
        {
            this.perPixelNormals.Override(perPixelNormals);
        }

        public Color GetAoColor()
        {
            return baseColor.value;
        }

        public void SetAoColor(Color baseColor)
        {
            this.baseColor.Override(baseColor);
        }

        public float GetAoIntensity()
        {
            return intensity.value;
        }

        public void SetAoIntensity(float intensity)
        {
            this.intensity.Override(Mathf.Clamp(intensity, this.intensity.min, this.intensity.max));
        }

        public bool UseMultiBounce()
        {
            return useMultiBounce.value;
        }

        public void EnableMultiBounce(bool enabled = true)
        {
            useMultiBounce.Override(enabled);
        }

        public float GetAoMultiBounceInfluence()
        {
            return multiBounceInfluence.value;
        }

        public void SetAoMultiBounceInfluence(float multiBounceInfluence)
        {
            this.multiBounceInfluence.Override(Mathf.Clamp(multiBounceInfluence, this.multiBounceInfluence.min, this.multiBounceInfluence.max));
        }

        public bool IsTemporalFilterEnabled()
        {
            return temporalFilterEnabled.value;
        }

        public void EnableTemporalFilter(bool enabled = true)
        {
            temporalFilterEnabled.Override(enabled);
        }

        public VarianceClipping GetTemporalFilterVarianceClipping()
        {
            return varianceClipping.value;
        }

        public void SetTemporalFilterVarianceClipping(VarianceClipping varianceClipping)
        {
            this.varianceClipping.Override(varianceClipping);
        }

        public BlurType GetBlurType()
        {
            return blurType.value;
        }

        public void SetBlurType(BlurType blurType)
        {
            this.blurType.Override(blurType);
        }

        public float GetBlurSharpness()
        {
            return sharpness.value;
        }

        public void SetBlurSharpness(float sharpness)
        {
            this.sharpness.Override(Mathf.Clamp(sharpness, this.sharpness.min, this.sharpness.max));
        }

        public bool IsColorBleedingEnabled()
        {
            return colorBleedingEnabled.value;
        }

        public void EnableColorBleeding(bool enabled = true)
        {
            colorBleedingEnabled.Override(enabled);
        }

        public float GetColorBleedingSaturation()
        {
            return saturation.value;
        }

        public void SetColorBleedingSaturation(float saturation)
        {
            this.saturation.Override(Mathf.Clamp(saturation, this.saturation.min, this.saturation.max));
        }

        public float GetColorBleedingBrightnessMask()
        {
            return brightnessMask.value;
        }

        public void SetColorBleedingBrightnessMask(float brightnessMask)
        {
            this.brightnessMask.Override(Mathf.Clamp(brightnessMask, this.brightnessMask.min, this.brightnessMask.max));
        }

        public Vector2 GetColorBleedingBrightnessMaskRange()
        {
            return brightnessMaskRange.value;
        }

        public void SetColorBleedingBrightnessMaskRange(Vector2 brightnessMaskRange)
        {
            brightnessMaskRange.x = Mathf.Clamp(brightnessMaskRange.x, this.brightnessMaskRange.min, this.brightnessMaskRange.max);
            brightnessMaskRange.y = Mathf.Clamp(brightnessMaskRange.y, this.brightnessMaskRange.min, this.brightnessMaskRange.max);
            brightnessMaskRange.x = Mathf.Min(brightnessMaskRange.x, brightnessMaskRange.y);
            this.brightnessMaskRange.Override(brightnessMaskRange);
        }

        public bool IsActive() => intensity.overrideState && intensity.value > 0;

        public bool IsTileCompatible() => true;
    }
}
