use crate::*;

use SemanticTokenVariant as SemVar;
use SemanticParseError as SemErr;
use SyntacticParseError as SynErr;


const NORMAL: &str = "\x1b[0m";
const BOLD:   &str = "\x1b[1m";
const DIM:    &str = "\x1b[2m";
const WHITE:  &str = "\x1b[37m";
const RED:    &str = "\x1b[31m";
const YELLOW: &str = "\x1b[33m";
const BLUE:   &str = "\x1b[34m";


pub struct Context<'a> {
    pub source_code: &'a str,
    pub source: &'a SourceSpan,
}


/// Print all errors found in the semantic tokens, including those inside macro
/// definitions. Returns true if at least one error was printed.
pub fn print_semantic_errors(semantic_tokens: &[SemanticToken], source_code: &str) -> bool {
    let mut found_error = false;
    for semantic_token in semantic_tokens {
        match &semantic_token.variant {
            SemVar::Error(err) => {
                let context = Context {
                    source_code: source_code,
                    source: &semantic_token.source,
                };
                found_error = true;
                print_semantic_error(&err, context)
            }
            SemVar::MacroDefinition(definition) => {
                for body_token in &definition.body_tokens {
                    if let SemVar::Error(err) = &body_token.variant {
                        let context = Context {
                            source_code: source_code,
                            source: &body_token.source,
                        };
                        found_error = true;
                        print_semantic_error(err, context)
                    }
                }
            }
            _ => (),
        }
    }
    return found_error;
}

fn print_semantic_error(error: &SemanticParseError, context: Context) {
    let message = get_message_for_semantic_error(error);
    print_error(&message, context);
}

fn get_message_for_semantic_error(error: &SemanticParseError) -> String {
    match error {
        SemErr::LabelDefinitionInMacroDefinition =>
            format!("Label cannot be defined inside a macro"),
        SemErr::MacroDefinitionInMacroDefinition =>
            format!("Macro cannot be defined inside a macro"),
        SemErr::StrayMacroTerminator =>
            format!("Macro definition terminator is missing a macro definition"),
        SemErr::StrayBlockClose =>
            format!("Block was not opened, add a '{{' character to open"),
        SemErr::UnclosedBlock =>
            format!("Block was not closed, add a '}}' character to close"),
        SemErr::UndefinedSymbol(name) =>
            format!("Undefined symbol, no label or macro has been defined with the name '{name}'"),
        SemErr::RedefinedSymbol((_, source)) =>
            format!("Redefined symbol, first defined at {}", source.location()),
        SemErr::MacroInvocationBeforeDefinition((_, source)) =>
            format!("Macro used before definition, definition is at {}", source.location()),
        SemErr:: SyntaxError(syntax_error) => match syntax_error {
            SynErr::UnterminatedComment =>
                format!("Unclosed comment, add a ')' character to close"),
            SynErr::UnterminatedRawString =>
                format!("Unclosed string, add a ' character to close"),
            SynErr::UnterminatedNullString =>
                format!("Unclosed string, add a \" character to close"),
            SynErr::InvalidPaddingValue(_) =>
                format!("Padding value must be two or four hexadecimal digits"),
        }
    }
}


pub fn print_resolver_errors(resolver: &SymbolResolver) -> bool {
    let mut found_error = false;
    for reference in &resolver.unresolved {
        found_error = true;
        let message = format!(
            "Undefined symbol, no label or macro has been defined with the name '{}'",
            &reference.symbol.source.string,
        );
        let source_code = resolver.get_source_code_for_tracked_symbol(reference);
        let source =  &reference.symbol.source;
        print_error(&message, Context { source_code, source } )
    }
    for redefinition in &resolver.redefinitions {
        found_error = true;
        let definition = resolver.definitions.get(redefinition.1).unwrap();
        let message = format!(
            "Redefined symbol, first defined at {}",
            &definition.symbol.source.in_merged,
        );
        let source_code = resolver.get_source_code_for_tracked_symbol(&redefinition.0);
        let source = &redefinition.0.symbol.source;
        print_error(&message, Context { source_code, source } )
    }
    return found_error;
}


/// The `ids` argument contains a list of the IDs of the source units which
/// cyclicly depend on one another.
pub fn print_cyclic_source_units(ids: &[usize], resolver: &SymbolResolver) {
    eprintln!("{BOLD}{RED}[ERROR]{WHITE}: Some libraries contain a dependency cycle{NORMAL}");
    for id in ids {
        if let Some(unit) = resolver.source_units.get(*id) {
            let path = &unit.source_unit.main.path;
            let path_str = path.as_os_str().to_string_lossy();
            if let Some(name_str) = get_unit_name(&unit.source_unit) {
                eprintln!("{name_str}{NORMAL}{DIM} ({path_str}){NORMAL}");
            } else {
                eprintln!("{path_str}");
            };
            // Print parents involved in dependency cycle.
            for parent_id in &unit.parent_ids {
                if !ids.contains(parent_id) { continue; }
                if let Some(parent_unit) = resolver.source_units.get(*parent_id) {
                    let parent_path = &parent_unit.source_unit.main.path;
                    let parent_path_str = parent_path.as_os_str().to_string_lossy();
                    let parent_name_str = match get_unit_name(&parent_unit.source_unit) {
                        Some(parent_name_str) => parent_name_str,
                        None => parent_path_str.to_string(),
                    };
                    eprintln!("  => {parent_name_str} {DIM}({parent_path_str}){NORMAL}");
                }
            }
        }
    }
}


pub fn print_error(message: &str, context: Context) {
    print_source_issue(message, context, SourceIssueVariant::Error);
}

pub fn print_warning(message: &str, context: Context) {
    print_source_issue(message, context, SourceIssueVariant::Warning);
}

fn print_source_issue(message: &str, context: Context, variant: SourceIssueVariant) {
    let (label, colour) = match variant {
        SourceIssueVariant::Warning => ("WARNING", YELLOW),
        SourceIssueVariant::Error => ("ERROR", RED),
    };

    // Prepare variables.
    let location = &context.source.in_merged;
    let y = location.start.line + 1;
    let digits = y.to_string().len();
    let arrow = "-->";
    let space = " ";

    // Print message and file path.
    eprintln!("{BOLD}{colour}[{label}]{WHITE}: {message}{NORMAL}");
    eprintln!("{BLUE}{arrow:>w$}{NORMAL} {location}{NORMAL}", w=digits+3);
    if let Some(source) = &context.source.in_source {
        eprintln!("{BLUE}{arrow:>w$}{NORMAL} {source}{NORMAL}", w=digits+3);
    }

    let start = location.start.column;
    let end = location.end.column + 1;

    // Print source code line.
    eprint!("{BLUE} {y} | {NORMAL}");
    let line = get_line_from_source_code(context.source_code, location.start.line);
    for (i, c) in line.chars().enumerate() {
        if i == start { eprint!("{colour}") }
        if i == end { eprint!("{NORMAL}") }
        eprint!("{c}");
    }
    eprintln!("{NORMAL}");

    // Print source code underline.
    eprint!("{BLUE} {space:>w$} | {NORMAL}", w=digits);
    for _ in 0..start { eprint!(" "); }
    eprint!("{colour}");
    for _ in start..end { eprint!("^"); }
    eprintln!("{NORMAL}");
}


fn get_line_from_source_code(source_code: &str, line: usize) -> &str {
    source_code.split('\n').nth(line).unwrap_or("<error reading line from source>")
}


enum SourceIssueVariant {
    Warning,
    Error,
}


/// Print a tree containing the name and path of each source unit.
pub fn print_source_tree(resolver: &SymbolResolver) {
    eprintln!(".");
    let len = resolver.root_unit_ids.len();
    for (i, id) in resolver.root_unit_ids.iter().enumerate() {
        let end = i + 1 == len;
        print_source_tree_leaf(resolver, *id, Vec::new(), end);
    }
    eprintln!();
}

fn print_source_tree_leaf(resolver: &SymbolResolver, id: usize, mut levels: Vec<bool>, end: bool) {
    // A level entry is true if all entries in that level have been printed.
    for level in &levels {
        match level {
            false => eprint!("│   "),
            true  => eprint!("    "),
        }
    }
    // The end value is true if all siblings of this entry have been printed.
    match end {
            false => eprint!("├── "),
            true  => eprint!("└── "),
    }
    if let Some(unit) = resolver.source_units.get(id) {
        let path_str = &unit.source_unit.main.path.as_os_str().to_string_lossy();
        if let Some(name_str) = get_unit_name(&unit.source_unit) {
            eprint!("{name_str}{BLUE}");
            if unit.source_unit.head.is_some() { eprint!(" +head") }
            if unit.source_unit.tail.is_some() { eprint!(" +tail") }
            let mut unresolved = 0;
            for symbol in &resolver.unresolved {
                if symbol.source_id == id { unresolved += 1; }
            }
            if unresolved > 0 { eprint!("{RED} ({unresolved})"); }
            eprintln!("{NORMAL} {DIM}({path_str}){NORMAL}");
        } else {
            eprintln!("{path_str}");
        }
        levels.push(end);
        let len = unit.child_ids.len();
        for (i, id) in unit.child_ids.iter().enumerate() {
            let end = i + 1 == len;
            print_source_tree_leaf(resolver, *id, levels.clone(), end);
        }
    } else {
        eprintln!("<error loading source unit details>");
    }
}


fn get_unit_name(source_unit: &SourceUnit) -> Option<String> {
    source_unit.main.path.file_name().map(|s| s.to_string_lossy().to_string())
}