Documentation
use thiserror::Error;

use crate::{
    json_value::{JsonValue, NumberValue},
    processor::Context,
    reader::Reader,
    selection::{Get, Result as SelectionResult},
};
use std::{io::Read, rc::Rc};

#[derive(Debug, Error)]
pub enum InputContextExtractorParseError {
    #[error("Input context type: '{0}' is unknown")]
    UnknownType(String),
}

#[derive(PartialEq, Debug)]
enum Type {
    Index,
    IndexInFile,
    FileName,
    StartedAtLineNumber,
    EndsAtLineNumber,
    StartedAtCharNumber,
    EndAtCharNumber,
}

struct InputContextExtractor {
    extraction: Type,
}

impl InputContextExtractor {
    fn from_name(name: String) -> Result<Self, InputContextExtractorParseError> {
        let extraction = match name.as_str() {
            "index" => Type::Index,
            "index-in-file" => Type::IndexInFile,
            "started-at-line-number" => Type::StartedAtLineNumber,
            "started-at-char-number" => Type::StartedAtCharNumber,
            "ended-at-line-number" => Type::EndsAtLineNumber,
            "ended-at-char-number" => Type::EndAtCharNumber,
            "file-name" => Type::FileName,
            _ => {
                return Err(InputContextExtractorParseError::UnknownType(name));
            }
        };
        Ok(Self { extraction })
    }
}

impl Get for InputContextExtractor {
    fn get(&self, value: &Context) -> Option<JsonValue> {
        if let Some(context) = value.input_context() {
            match self.extraction {
                Type::Index => Some(JsonValue::Number(NumberValue::Positive(context.index))),
                Type::IndexInFile => {
                    Some(JsonValue::Number(NumberValue::Positive(context.file_index)))
                }
                Type::StartedAtLineNumber => Some(context.start_location.line_number.into()),
                Type::EndsAtLineNumber => Some(context.end_location.line_number.into()),
                Type::StartedAtCharNumber => Some(context.start_location.char_number.into()),
                Type::EndAtCharNumber => Some(context.end_location.char_number.into()),
                Type::FileName => context
                    .start_location
                    .input
                    .as_ref()
                    .map(|str| str.clone().into()),
            }
        } else {
            None
        }
    }
}

pub fn parse_input_context<R: Read>(reader: &mut Reader<R>) -> SelectionResult<Rc<dyn Get>> {
    let mut name = Vec::new();
    while let Some(ch) = reader.next()? {
        if ch.is_ascii_lowercase() {
            name.push(ch);
        } else if ch.is_ascii_uppercase() {
            name.push(ch.to_ascii_lowercase());
        } else if ch == b'_' || ch == b'-' {
            name.push(b'-');
        } else {
            break;
        }
    }
    let name = String::from_utf8(name)?;
    let getter = InputContextExtractor::from_name(name)?;
    Ok(Rc::new(getter))
}

#[cfg(test)]
mod tests {
    use crate::{
        reader::{Location, from_string},
        regex_cache::RegexCache,
    };

    use super::*;

    #[test]
    fn from_name_return_the_correct_name_index() -> SelectionResult<()> {
        from_name_return_the_correct_name("index", &Type::Index)
    }

    #[test]
    fn from_name_return_the_correct_name_index_in_file() -> SelectionResult<()> {
        from_name_return_the_correct_name("index-in-file", &Type::IndexInFile)
    }

    #[test]
    fn from_name_return_the_correct_name_started_line() -> SelectionResult<()> {
        from_name_return_the_correct_name("started-at-line-number", &Type::StartedAtLineNumber)
    }

    #[test]
    fn from_name_return_the_correct_name_ended_line() -> SelectionResult<()> {
        from_name_return_the_correct_name("ended-at-line-number", &Type::EndsAtLineNumber)
    }

    #[test]
    fn from_name_return_the_correct_name_started_char() -> SelectionResult<()> {
        from_name_return_the_correct_name("started-at-char-number", &Type::StartedAtCharNumber)
    }

    #[test]
    fn from_name_return_the_correct_name_ended_char() -> SelectionResult<()> {
        from_name_return_the_correct_name("ended-at-char-number", &Type::EndAtCharNumber)
    }

    #[test]
    fn from_name_return_the_correct_name_file_name() -> SelectionResult<()> {
        from_name_return_the_correct_name("file-name", &Type::FileName)
    }

    fn from_name_return_the_correct_name(name: &str, expected: &Type) -> SelectionResult<()> {
        let got = InputContextExtractor::from_name(name.to_string())?.extraction;

        assert_eq!(&got, expected);

        Ok(())
    }

    #[test]
    fn from_name_return_error_for_unknown_name() {
        let err = InputContextExtractor::from_name("nop".to_string()).err();

        assert!(err.is_some());
    }

    #[test]
    fn get_return_the_correct_values_index() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("index".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 40,
                char_number: 10,
            },
            Location {
                input: None,
                line_number: 40,
                char_number: 10,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some((61).into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_index_in_file() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("index-in-file".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 40,
                char_number: 10,
            },
            Location {
                input: None,
                line_number: 40,
                char_number: 10,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some((10).into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_line_started() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("started-at-line-number".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 41,
                char_number: 10,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 10,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some((41).into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_line_ended() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("ended-at-line-number".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 41,
                char_number: 10,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 10,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some((42).into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_char_started() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("started-at-char-number".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 41,
                char_number: 11,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 20,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some((11).into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_char_ended() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("ended-at-char-number".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 41,
                char_number: 11,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 20,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some((20).into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_file_name() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("file-name".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: Some("test".into()),
                line_number: 41,
                char_number: 11,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 20,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), Some("test".into()));

        Ok(())
    }

    #[test]
    fn get_return_the_correct_values_file_name_none() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("file-name".to_string())?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 41,
                char_number: 11,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 20,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(ext.get(&context), None);

        Ok(())
    }

    #[test]
    fn get_return_notging_without_context() -> SelectionResult<()> {
        let ext = InputContextExtractor::from_name("index".to_string())?;
        let context = Context::new_with_no_context(JsonValue::Null);

        assert_eq!(ext.get(&context), None);

        Ok(())
    }

    #[test]
    fn parse_will_be_case_insensitive() -> SelectionResult<()> {
        let text = "EndEd-at_char-number 3".into();
        let mut reader = from_string(&text);

        let selection = parse_input_context(&mut reader)?;
        let context = Context::new_with_input(
            JsonValue::Null,
            Location {
                input: None,
                line_number: 41,
                char_number: 11,
            },
            Location {
                input: None,
                line_number: 42,
                char_number: 20,
            },
            10,
            61,
            &RegexCache::new(0),
        );

        assert_eq!(selection.get(&context), Some((20).into()));

        Ok(())
    }

    #[test]
    fn parse_will_create_exception_for_unknown_name() {
        let text = "test".into();
        let mut reader = from_string(&text);

        let err = parse_input_context(&mut reader).err();

        assert!(err.is_some());
    }
}