use super::{
    Type, ValueConstructor, ValueConstructorVariant,
    error::{Error, UnifyErrorSituation},
    expr::ExprTyper,
};
use crate::{
    ast::{AssignmentKind, CallArg, PIPE_VARIABLE, Pattern, Span},
    expr::{TypedExpr, UntypedExpr},
};
use std::{ops::Deref, rc::Rc};
use vec1::Vec1;

#[derive(Debug)]
pub(crate) struct PipeTyper<'a, 'b, 'c> {
    size: usize,
    argument_type: Rc<Type>,
    argument_location: Span,
    location: Span,
    expressions: Vec<TypedExpr>,
    expr_typer: &'a mut ExprTyper<'b, 'c>,
}

impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> {
    #[allow(clippy::result_large_err)]
    pub fn infer(
        expr_typer: &'a mut ExprTyper<'b, 'c>,
        expressions: Vec1<UntypedExpr>,
    ) -> Result<TypedExpr, Error> {
        let size = expressions.len();

        let end = &expressions[..]
            .last()
            // The vec is non-empty, this indexing can never fail
            .expect("Empty pipeline in typer")
            .location()
            .end;

        let mut expressions = expressions.into_iter();

        let first = expr_typer.infer(expressions.next().expect("Empty pipeline in typer"))?;

        let mut typer = Self {
            size,
            expr_typer,
            argument_type: first.tipo(),
            argument_location: first.location(),
            location: Span {
                start: first.location().start,
                end: *end,
            },
            expressions: Vec::with_capacity(size),
        };
        // No need to update self.argument_* as we set it above
        typer.push_assignment_no_update(first);

        // Perform the type checking
        typer.infer_expressions(expressions)
    }

    #[allow(clippy::result_large_err)]
    fn infer_expressions(
        mut self,
        expressions: impl IntoIterator<Item = UntypedExpr>,
    ) -> Result<TypedExpr, Error> {
        let result = self.infer_each_expression(expressions);

        // Clean-up the pipe variables inserted so they cannot be used outside this pipeline
        let _ = self.expr_typer.environment.scope.remove(PIPE_VARIABLE);

        // Return any errors after clean-up
        result?;

        Ok(TypedExpr::Pipeline {
            expressions: self.expressions,
            location: self.location,
        })
    }

    #[allow(clippy::result_large_err)]
    fn infer_each_expression(
        &mut self,
        expressions: impl IntoIterator<Item = UntypedExpr>,
    ) -> Result<(), Error> {
        for (i, call) in expressions.into_iter().enumerate() {
            let call = match call {
                // left |> right(..args)
                UntypedExpr::Call {
                    fun,
                    arguments,
                    location,
                    ..
                } => {
                    let fun = self.expr_typer.infer(*fun)?;

                    match fun.tipo().fn_arity() {
                        // Rewrite as right(left, ..args)
                        Some(arity) if arguments.len() < arity => {
                            self.infer_insert_pipe(fun, arguments, location)?
                        }

                        // Rewrite as right(..args)(left)
                        _ => self.infer_apply_to_call_pipe(fun, arguments, location)?,
                    }
                }

                // right(left)
                call => self.infer_apply_pipe(call)?,
            };

            if i + 2 == self.size {
                self.expressions.push(call);
            } else {
                self.push_assignment(call);
            }
        }

        Ok(())
    }

    /// Create a call argument that can be used to refer to the value on the
    /// left hand side of the pipe
    fn typed_left_hand_value_variable_call_argument(&self) -> CallArg<TypedExpr> {
        CallArg {
            label: None,
            location: self.argument_location,
            value: self.typed_left_hand_value_variable(),
        }
    }

    /// Create a call argument that can be used to refer to the value on the
    /// left hand side of the pipe
    fn untyped_left_hand_value_variable_call_argument(&self) -> CallArg<UntypedExpr> {
        CallArg {
            label: None,
            location: self.argument_location,
            value: self.untyped_left_hand_value_variable(),
        }
    }

    /// Create a variable that can be used to refer to the value on the left
    /// hand side of the pipe
    fn typed_left_hand_value_variable(&self) -> TypedExpr {
        TypedExpr::Var {
            location: self.argument_location,
            name: PIPE_VARIABLE.to_string(),
            constructor: ValueConstructor {
                public: true,
                tipo: self.argument_type.clone(),
                variant: ValueConstructorVariant::LocalVariable {
                    location: self.argument_location,
                },
            },
        }
    }

    /// Create a variable that can be used to refer to the value on the left
    /// hand side of the pipe
    fn untyped_left_hand_value_variable(&self) -> UntypedExpr {
        UntypedExpr::Var {
            location: self.argument_location,
            name: PIPE_VARIABLE.to_string(),
        }
    }

    /// Push an assignment for the value on the left hand side of the pipe
    fn push_assignment(&mut self, expression: TypedExpr) {
        self.argument_type = expression.tipo();
        self.argument_location = expression.location();
        self.push_assignment_no_update(expression)
    }

    fn push_assignment_no_update(&mut self, expression: TypedExpr) {
        let location = expression.location();

        // Insert the variable for use in type checking the rest of the pipeline
        self.expr_typer.environment.insert_variable(
            PIPE_VARIABLE.to_string(),
            ValueConstructorVariant::LocalVariable { location },
            expression.tipo(),
        );

        // Add the assignment to the AST
        let assignment = TypedExpr::Assignment {
            location,
            tipo: expression.tipo(),
            kind: AssignmentKind::let_(),
            value: Box::new(expression),
            pattern: Pattern::Var {
                location,
                name: PIPE_VARIABLE.to_string(),
            },
            comment: None,
        };

        self.expressions.push(assignment);
    }

    /// Attempt to infer a |> b(..c) as b(..c)(a)
    #[allow(clippy::result_large_err)]
    fn infer_apply_to_call_pipe(
        &mut self,
        function: TypedExpr,
        args: Vec<CallArg<UntypedExpr>>,
        location: Span,
    ) -> Result<TypedExpr, Error> {
        let (function, args, tipo) =
            self.expr_typer
                .do_infer_call_with_known_fun(function, args, location, |e| e)?;

        let function = TypedExpr::Call {
            location,
            tipo,
            args,
            fun: Box::new(function),
        };

        let args = vec![self.untyped_left_hand_value_variable_call_argument()];
        let (function, args, tipo) =
            self.expr_typer
                .do_infer_call_with_known_fun(function, args, location, |e| {
                    e.with_unify_error_situation(UnifyErrorSituation::PipeTypeMismatch)
                })?;

        Ok(TypedExpr::Call {
            location,
            tipo,
            args,
            fun: Box::new(function),
        })
    }

    /// Attempt to infer a |> b(c) as b(a, c)
    #[allow(clippy::result_large_err)]
    fn infer_insert_pipe(
        &mut self,
        function: TypedExpr,
        mut arguments: Vec<CallArg<UntypedExpr>>,
        location: Span,
    ) -> Result<TypedExpr, Error> {
        arguments.insert(0, self.untyped_left_hand_value_variable_call_argument());
        let (fun, args, tipo) =
            self.expr_typer
                .do_infer_call_with_known_fun(function, arguments, location, |e| {
                    e.with_unify_error_situation(UnifyErrorSituation::PipeTypeMismatch)
                })?;

        Ok(TypedExpr::Call {
            location,
            tipo,
            args,
            fun: Box::new(fun),
        })
    }

    /// Attempt to infer a |> b as b(a)
    #[allow(clippy::result_large_err)]
    fn infer_apply_pipe(&mut self, func: UntypedExpr) -> Result<TypedExpr, Error> {
        let func = Box::new(self.expr_typer.infer(func)?);
        let return_type = self.expr_typer.new_unbound_var();

        // Ensure that the function accepts one argument of the correct type
        self.expr_typer
            .environment
            .unify(
                func.tipo(),
                Type::function(vec![self.argument_type.clone()], return_type.clone()),
                func.location(),
                if let Type::Fn { args, .. } = func.tipo().deref() {
                    if let Some(typ) = args.first() {
                        typ.is_data()
                    } else {
                        false
                    }
                } else {
                    false
                },
            )
            .map_err(|e| {
                let is_pipe_mismatch = self.check_if_pipe_type_mismatch(&e, func.location());

                if is_pipe_mismatch {
                    e.with_unify_error_situation(UnifyErrorSituation::PipeTypeMismatch)
                } else {
                    e
                }
            })?;

        Ok(TypedExpr::Call {
            location: func.location(),
            tipo: return_type,
            fun: func,
            args: vec![self.typed_left_hand_value_variable_call_argument()],
        })
    }

    fn check_if_pipe_type_mismatch(&mut self, error: &Error, location: Span) -> bool {
        let types = match error {
            Error::CouldNotUnify {
                expected, given, ..
            } => (expected.as_ref(), given.as_ref()),
            _ => return false,
        };

        match types {
            (Type::Fn { args: a, .. }, Type::Fn { args: b, .. }) if a.len() == b.len() => {
                match (a.first(), b.first()) {
                    (Some(a), Some(b)) => self
                        .expr_typer
                        .environment
                        .unify(a.clone(), b.clone(), location, a.is_data())
                        .is_err(),
                    _ => false,
                }
            }
            _ => false,
        }
    }
}
