use std::{iter, ops};
use crate::nodes::{
    Block, Expression, FieldExpression, FunctionCall, Identifier, InterpolatedStringExpression,
    InterpolationSegment, LocalAssignStatement, Prefix, StringExpression, TupleArguments,
    TypedIdentifier,
};
use crate::process::{IdentifierTracker, NodeProcessor, NodeVisitor, ScopeVisitor};
use crate::rules::{
    Context, FlawlessRule, RuleConfiguration, RuleConfigurationError, RuleProperties,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ReplacementStrategy {
    StringSpecifier,
    ToStringSpecifier,
}
impl Default for ReplacementStrategy {
    fn default() -> Self {
        Self::StringSpecifier
    }
}
struct RemoveInterpolatedStringProcessor {
    string_format_identifier: String,
    tostring_identifier: String,
    define_string_format: bool,
    define_tostring: bool,
    identifier_tracker: IdentifierTracker,
    strategy: ReplacementStrategy,
}
impl ops::Deref for RemoveInterpolatedStringProcessor {
    type Target = IdentifierTracker;
    fn deref(&self) -> &Self::Target {
        &self.identifier_tracker
    }
}
impl ops::DerefMut for RemoveInterpolatedStringProcessor {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.identifier_tracker
    }
}
const DEFAULT_TOSTRING_IDENTIFIER: &str = "tostring";
const DEFAULT_STRING_LIBRARY: &str = "string";
const DEFAULT_STRING_FORMAT_NAME: &str = "format";
impl RemoveInterpolatedStringProcessor {
    fn new(
        strategy: ReplacementStrategy,
        string_format_identifier: impl Into<String>,
        tostring_identifier: impl Into<String>,
    ) -> Self {
        Self {
            string_format_identifier: string_format_identifier.into(),
            tostring_identifier: tostring_identifier.into(),
            define_string_format: false,
            define_tostring: false,
            identifier_tracker: Default::default(),
            strategy,
        }
    }
    fn replace_with(&mut self, string: &InterpolatedStringExpression) -> Expression {
        if string.is_empty() {
            StringExpression::from_value("").into()
        } else if string.len() == 1 {
            match string.iter_segments().next().unwrap() {
                InterpolationSegment::String(string_segment) => {
                    StringExpression::from_value(string_segment.get_value()).into()
                }
                InterpolationSegment::Value(value_segment) => FunctionCall::from_name(
                    if self.is_identifier_used(DEFAULT_TOSTRING_IDENTIFIER) {
                        self.define_tostring = true;
                        &self.tostring_identifier
                    } else {
                        DEFAULT_TOSTRING_IDENTIFIER
                    },
                )
                .with_argument(value_segment.get_expression().clone())
                .into(),
            }
        } else {
            let arguments = iter::once(
                StringExpression::from_value(string.iter_segments().fold(
                    String::new(),
                    |mut format_string, segment| {
                        match segment {
                            InterpolationSegment::String(string_segment) => {
                                format_string
                                    .push_str(&string_segment.get_value().replace('%', "%%"));
                            }
                            InterpolationSegment::Value(_) => {
                                format_string.push_str(match self.strategy {
                                    ReplacementStrategy::StringSpecifier => "%s",
                                    ReplacementStrategy::ToStringSpecifier => "%*",
                                });
                            }
                        }
                        format_string
                    },
                ))
                .into(),
            )
            .chain(
                string
                    .iter_segments()
                    .filter_map(|segment| match segment {
                        InterpolationSegment::Value(segment) => {
                            Some(segment.get_expression().clone())
                        }
                        InterpolationSegment::String(_) => None,
                    })
                    .map(|value| match self.strategy {
                        ReplacementStrategy::ToStringSpecifier => value,
                        ReplacementStrategy::StringSpecifier => FunctionCall::from_name(
                            if self.is_identifier_used(DEFAULT_TOSTRING_IDENTIFIER) {
                                self.define_tostring = true;
                                &self.tostring_identifier
                            } else {
                                DEFAULT_TOSTRING_IDENTIFIER
                            },
                        )
                        .with_argument(value)
                        .into(),
                    }),
            )
            .collect::<TupleArguments>();
            FunctionCall::from_prefix(if self.is_identifier_used(DEFAULT_STRING_LIBRARY) {
                self.define_string_format = true;
                Prefix::from_name(&self.string_format_identifier)
            } else {
                FieldExpression::new(
                    Prefix::from_name(DEFAULT_STRING_LIBRARY),
                    DEFAULT_STRING_FORMAT_NAME,
                )
                .into()
            })
            .with_arguments(arguments)
            .into()
        }
    }
}
impl NodeProcessor for RemoveInterpolatedStringProcessor {
    fn process_expression(&mut self, expression: &mut Expression) {
        if let Expression::InterpolatedString(string) = expression {
            *expression = self.replace_with(string);
        }
    }
}
pub const REMOVE_INTERPOLATED_STRING_RULE_NAME: &str = "remove_interpolated_string";
#[derive(Debug, Default, PartialEq, Eq)]
pub struct RemoveInterpolatedString {
    strategy: ReplacementStrategy,
}
impl FlawlessRule for RemoveInterpolatedString {
    fn flawless_process(&self, block: &mut Block, _: &Context) {
        const STRING_FORMAT_IDENTIFIER: &str = "__DARKLUA_STR_FMT";
        const TOSTRING_IDENTIFIER: &str = "__DARKLUA_TO_STR";
        let mut processor = RemoveInterpolatedStringProcessor::new(
            self.strategy,
            STRING_FORMAT_IDENTIFIER,
            TOSTRING_IDENTIFIER,
        );
        ScopeVisitor::visit_block(block, &mut processor);
        if processor.define_string_format || processor.define_tostring {
            let mut variables = Vec::new();
            let mut values = Vec::new();
            if processor.define_string_format {
                variables.push(TypedIdentifier::new(STRING_FORMAT_IDENTIFIER));
                values.push(
                    FieldExpression::new(
                        Prefix::from_name(DEFAULT_STRING_LIBRARY),
                        DEFAULT_STRING_FORMAT_NAME,
                    )
                    .into(),
                );
            }
            if processor.define_tostring {
                variables.push(TypedIdentifier::new(TOSTRING_IDENTIFIER));
                values.push(Identifier::new(DEFAULT_TOSTRING_IDENTIFIER).into());
            }
            block.insert_statement(0, LocalAssignStatement::new(variables, values));
        }
    }
}
impl RuleConfiguration for RemoveInterpolatedString {
    fn configure(&mut self, properties: RuleProperties) -> Result<(), RuleConfigurationError> {
        for (key, value) in properties {
            match key.as_str() {
                "strategy" => {
                    self.strategy = match value.expect_string(&key)?.as_str() {
                        "string" => ReplacementStrategy::StringSpecifier,
                        "tostring" => ReplacementStrategy::ToStringSpecifier,
                        unexpected => {
                            return Err(RuleConfigurationError::UnexpectedValue {
                                property: "strategy".to_owned(),
                                message: format!(
                                    "invalid value `{}` (must be `string` or `tostring`)",
                                    unexpected
                                ),
                            })
                        }
                    };
                }
                _ => return Err(RuleConfigurationError::UnexpectedProperty(key)),
            }
        }
        Ok(())
    }
    fn get_name(&self) -> &'static str {
        REMOVE_INTERPOLATED_STRING_RULE_NAME
    }
    fn serialize_to_properties(&self) -> RuleProperties {
        let mut properties = RuleProperties::new();
        match self.strategy {
            ReplacementStrategy::StringSpecifier => {}
            ReplacementStrategy::ToStringSpecifier => {
                properties.insert("strategy".to_owned(), "tostring".into());
            }
        }
        properties
    }
}
#[cfg(test)]
mod test {
    use super::*;
    use crate::rules::Rule;
    use insta::assert_json_snapshot;
    fn new_rule() -> RemoveInterpolatedString {
        RemoveInterpolatedString::default()
    }
    #[test]
    fn serialize_default_rule() {
        let rule: Box<dyn Rule> = Box::new(new_rule());
        assert_json_snapshot!("default_remove_interpolated_string", rule);
    }
    #[test]
    fn serialize_rule_with_tostring_strategy() {
        let rule: Box<dyn Rule> = Box::new(RemoveInterpolatedString {
            strategy: ReplacementStrategy::ToStringSpecifier,
        });
        assert_json_snapshot!("remove_interpolated_string_tostring_strategy", rule);
    }
    #[test]
    fn configure_with_extra_field_error() {
        let result = json5::from_str::<Box<dyn Rule>>(
            r#"{
            rule: 'remove_interpolated_string',
            prop: "something",
        }"#,
        );
        pretty_assertions::assert_eq!(result.unwrap_err().to_string(), "unexpected field 'prop'");
    }
}