// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you 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.

//! See `main.rs` for how to run it.
//!
//! This example demonstrates how to use the
//! `PhysicalProtoConverterExtension` trait's interception methods to
//! implement expression deduplication during deserialization.
//!
//! This pattern is inspired by PR #18192, which introduces expression caching
//! to reduce memory usage when deserializing plans with duplicate expressions.
//!
//! The key insight is that identical expressions serialize to identical protobuf bytes.
//! By caching deserialized expressions keyed by their protobuf bytes, we can:
//! 1. Return the same Arc for duplicate expressions
//! 2. Reduce memory allocation during deserialization
//! 3. Enable downstream optimizations that rely on Arc pointer equality
//!
//! This demonstrates the decorator pattern enabled by
//! `PhysicalProtoConverterExtension`, where physical-expression
//! serialization and deserialization route through converter hooks.

use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::{Arc, RwLock};

use arrow::datatypes::{DataType, Field, Schema};
use datafusion::common::Result;
use datafusion::execution::TaskContext;
use datafusion::logical_expr::Operator;
use datafusion::physical_expr::PhysicalExpr;
use datafusion::physical_plan::ExecutionPlan;
use datafusion::physical_plan::expressions::{BinaryExpr, col};
use datafusion::physical_plan::filter::FilterExec;
use datafusion::physical_plan::placeholder_row::PlaceholderRowExec;
use datafusion::prelude::SessionContext;
use datafusion_proto::physical_plan::from_proto::parse_physical_expr_with_converter;
use datafusion_proto::physical_plan::to_proto::serialize_physical_expr_with_converter;
use datafusion_proto::physical_plan::{
    DefaultPhysicalExtensionCodec, PhysicalExtensionCodec, PhysicalPlanDecodeContext,
    PhysicalProtoConverterExtension,
};
use datafusion_proto::protobuf::{PhysicalExprNode, PhysicalPlanNode};
use prost::Message;

/// Example showing how to implement expression deduplication using the codec decorator pattern.
///
/// This demonstrates:
/// 1. Creating a CachingCodec that caches expressions by their protobuf bytes
/// 2. Intercepting deserialization to return cached Arcs for duplicate expressions
/// 3. Verifying that duplicate expressions share the same Arc after deserialization
///
/// Deduplication is keyed by the protobuf bytes representing the expression,
/// in reality deduplication could be done based on e.g. the pointer address of the
/// serialized expression in memory, but this is simpler to demonstrate.
///
/// In this case our expression is trivial and just for demonstration purposes.
/// In real scenarios, expressions can be much more complex, e.g. a large InList
/// expression could be megabytes in size, so deduplication can save significant memory
/// in addition to more correctly representing the original plan structure.
pub async fn expression_deduplication() -> Result<()> {
    println!("=== Expression Deduplication Example ===\n");

    // Create a schema for our test expressions
    let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Boolean, false)]));

    // Step 1: Create expressions with duplicates
    println!("Step 1: Creating expressions with duplicates...");

    // Create expression: col("a")
    let a = col("a", &schema)?;

    // Create a clone to show duplicates
    let a_clone = Arc::clone(&a);

    // Combine: a OR a_clone
    let combined_expr =
        Arc::new(BinaryExpr::new(a, Operator::Or, a_clone)) as Arc<dyn PhysicalExpr>;
    println!("  Created expression: a OR a with duplicates");
    println!("  Note: a appears twice in the expression tree\n");
    // Step 2: Create a filter plan with this expression
    println!("Step 2: Creating physical plan with the expression...");

    let input = Arc::new(PlaceholderRowExec::new(Arc::clone(&schema)));
    let filter_plan: Arc<dyn ExecutionPlan> =
        Arc::new(FilterExec::try_new(combined_expr, input)?);

    println!("  Created FilterExec with duplicate sub-expressions\n");

    // Step 3: Serialize with the caching codec
    println!("Step 3: Serializing plan...");

    let extension_codec = DefaultPhysicalExtensionCodec {};
    let caching_converter = CachingCodec::new();
    let proto =
        caching_converter.execution_plan_to_proto(&filter_plan, &extension_codec)?;

    // Serialize to bytes
    let mut bytes = Vec::new();
    proto.encode(&mut bytes).unwrap();
    println!("  Serialized plan to {} bytes\n", bytes.len());

    // Step 4: Deserialize with the caching codec
    println!("Step 4: Deserializing plan with CachingCodec...");

    let ctx = SessionContext::new();
    let deserialized_plan = proto.try_into_physical_plan_with_converter(
        &ctx.task_ctx(),
        &extension_codec,
        &caching_converter,
    )?;

    // Step 5: check that we deduplicated expressions
    println!("Step 5: Checking for deduplicated expressions...");
    let Some(filter_exec) = deserialized_plan.downcast_ref::<FilterExec>() else {
        panic!("Deserialized plan is not a FilterExec");
    };
    let predicate = Arc::clone(filter_exec.predicate());
    let binary_expr = predicate
        .downcast_ref::<BinaryExpr>()
        .expect("Predicate is not a BinaryExpr");
    let left = &binary_expr.left();
    let right = &binary_expr.right();
    // Check if left and right point to the same Arc
    let deduplicated = Arc::ptr_eq(left, right);
    if deduplicated {
        println!("  Success: Duplicate expressions were deduplicated!");
        println!(
            "  Cache Stats: hits={}, misses={}",
            caching_converter.stats.read().unwrap().cache_hits,
            caching_converter.stats.read().unwrap().cache_misses,
        );
    } else {
        println!("  Failure: Duplicate expressions were NOT deduplicated.");
    }

    Ok(())
}

// ============================================================================
// CachingCodec - Implements expression deduplication
// ============================================================================

/// Statistics for cache performance monitoring
#[derive(Debug, Default)]
struct CacheStats {
    cache_hits: usize,
    cache_misses: usize,
}

/// A codec that caches deserialized expressions to enable deduplication.
///
/// When deserializing, if we've already seen the same protobuf bytes,
/// we return the cached Arc instead of creating a new allocation.
#[derive(Debug, Default)]
struct CachingCodec {
    /// Cache mapping protobuf bytes -> deserialized expression
    expr_cache: RwLock<HashMap<Vec<u8>, Arc<dyn PhysicalExpr>>>,
    /// Statistics for demonstration
    stats: RwLock<CacheStats>,
}

impl CachingCodec {
    fn new() -> Self {
        Self::default()
    }
}

impl PhysicalExtensionCodec for CachingCodec {
    // Required: decode custom extension nodes
    fn try_decode(
        &self,
        _buf: &[u8],
        _inputs: &[Arc<dyn ExecutionPlan>],
        _ctx: &TaskContext,
    ) -> Result<Arc<dyn ExecutionPlan>> {
        datafusion::common::not_impl_err!("No custom extension nodes")
    }

    // Required: encode custom execution plans
    fn try_encode(
        &self,
        _node: Arc<dyn ExecutionPlan>,
        _buf: &mut Vec<u8>,
    ) -> Result<()> {
        datafusion::common::not_impl_err!("No custom extension nodes")
    }
}

impl PhysicalProtoConverterExtension for CachingCodec {
    fn proto_to_execution_plan(
        &self,
        proto: &PhysicalPlanNode,
        ctx: &PhysicalPlanDecodeContext<'_>,
    ) -> Result<Arc<dyn ExecutionPlan>> {
        self.default_proto_to_execution_plan(proto, ctx)
    }

    fn execution_plan_to_proto(
        &self,
        plan: &Arc<dyn ExecutionPlan>,
        extension_codec: &dyn PhysicalExtensionCodec,
    ) -> Result<PhysicalPlanNode> {
        PhysicalPlanNode::try_from_physical_plan_with_converter(
            Arc::clone(plan),
            extension_codec,
            self,
        )
    }

    // CACHING IMPLEMENTATION: Intercept expression deserialization
    fn proto_to_physical_expr(
        &self,
        proto: &PhysicalExprNode,
        input_schema: &Schema,
        ctx: &PhysicalPlanDecodeContext<'_>,
    ) -> Result<Arc<dyn PhysicalExpr>> {
        // Create cache key from protobuf bytes
        let mut key = Vec::new();
        proto.encode(&mut key).map_err(|e| {
            datafusion::error::DataFusionError::Internal(format!(
                "Failed to encode proto for cache key: {e}"
            ))
        })?;

        // Check cache first
        {
            let cache = self.expr_cache.read().unwrap();
            if let Some(cached) = cache.get(&key) {
                // Cache hit! Update stats and return cached Arc
                let mut stats = self.stats.write().unwrap();
                stats.cache_hits += 1;
                return Ok(Arc::clone(cached));
            }
        }

        // Cache miss - deserialize and store
        let expr = parse_physical_expr_with_converter(proto, input_schema, ctx, self)?;

        // Store in cache
        {
            let mut cache = self.expr_cache.write().unwrap();
            cache.insert(key, Arc::clone(&expr));
            let mut stats = self.stats.write().unwrap();
            stats.cache_misses += 1;
        }

        Ok(expr)
    }

    fn physical_expr_to_proto(
        &self,
        expr: &Arc<dyn PhysicalExpr>,
        codec: &dyn PhysicalExtensionCodec,
    ) -> Result<PhysicalExprNode> {
        serialize_physical_expr_with_converter(expr, codec, self)
    }
}
