use crate::*;


/// The entire semantic program, ready to generate bytecode.
pub struct Program {
    pub definitions: Vec<Definition>,
    pub invocations: Vec<Invocation>,
    pub errors: Vec<ParseError>,
}

/// A symbol definition.
pub struct Definition {
    pub name: String,
    pub source: SourceSpan,
    pub arguments: Vec<ArgumentDefinition>,
    pub variant: DefinitionVariant,
}

pub struct ArgumentDefinition {
    pub name: String,
    pub source: SourceSpan,
    pub variant: ArgumentDefinitionVariant,
}

pub enum ArgumentDefinitionVariant {
    Integer,
    Block,
}

pub enum DefinitionVariant {
    Integer(IntegerDefinition),
    Block(BlockDefinition),
    Reference(ReferenceDefinition),
}

pub struct IntegerDefinition {
    pub source: SourceSpan,
    pub variant: IntegerDefinitionVariant,
}

pub enum IntegerDefinitionVariant {
    Literal(usize),
    Constant(ConstantExpression),
}

pub struct BlockDefinition {
    pub tokens: Vec<BlockToken>,
    pub errors: Vec<ParseError>,
}

pub struct BlockToken {
    pub source: SourceSpan,
    pub variant: BlockTokenVariant,
}

pub enum BlockTokenVariant {
    Invocation(Invocation),
    Comment(String),
    Word(PackedBinaryLiteral),
}

/// References aren't necessarily an integer or a block
pub struct ReferenceDefinition {
    pub source: SourceSpan,
    pub name: String,
}

pub struct Invocation {
    pub name: String,
    pub arguments: Vec<DefinitionVariant>,
    pub errors: Vec<ParseError>,
}

pub struct ParseError {
    pub source: SourceSpan,
    pub variant: ParseErrorVariant,
}

pub enum ParseErrorVariant {
    UnterminatedMacroDefinition(String),
    UnterminatedBlockDefinition,
    /// Name of the macro.
    InvalidArgumentDefinition(String),
    InvalidToken,
}


// ------------------------------------------------------------------------ //

macro_rules! indent {
    ($indent:expr => $($tokens:tt)*) => {{
        for _ in 0..$indent { print!("  "); }
        println!($($tokens)*);
    }};
}

impl Program {
    pub fn print_definitions(&self) {
        for definition in &self.definitions {
            let variant = match &definition.variant {
                DefinitionVariant::Integer(_) => "INTEGER",
                DefinitionVariant::Block(_) => "BLOCK",
                DefinitionVariant::Reference(_) => "REFERENCE",
            };
            println!("DEFINE {variant} '{}'", definition.name);
            for argument in &definition.arguments {
                self.print_argument_definition(argument);
            }
            match &definition.variant {
                DefinitionVariant::Integer(integer) =>
                    self.print_integer_definition(1, integer),
                DefinitionVariant::Block(block) =>
                    self.print_block_definition(1, block),
                DefinitionVariant::Reference(reference) =>
                    indent!(1 => "REFERENCE '{}'", reference.name),
            };
            println!();
        }

        for invocation in &self.invocations {
            self.print_invocation(0, invocation);
        }
    }

    fn print_argument_definition(&self, argument: &ArgumentDefinition) {
        let variant = match argument.variant {
            ArgumentDefinitionVariant::Integer => "INTEGER",
            ArgumentDefinitionVariant::Block => "BLOCK",
        };
        println!("  ARGUMENT {variant} '{}'", argument.name);
    }

    fn print_integer_definition(&self, indent: usize, definition: &IntegerDefinition) {
        match &definition.variant {
            IntegerDefinitionVariant::Literal(value) =>
                indent!(indent => "LITERAL {value}"),
            IntegerDefinitionVariant::Constant(expr) =>
                indent!(indent => "CONSTANT [{expr:?}]"),
        }
    }

    fn print_block_definition(&self, indent: usize, definition: &BlockDefinition) {
        indent!(indent => "BLOCK");
        let indent = indent + 1;
        for token in &definition.tokens {
            match &token.variant {
                BlockTokenVariant::Invocation(invocation) =>
                    self.print_invocation(indent, invocation),
                BlockTokenVariant::Comment(_) =>
                    indent!(indent => "COMMENT"),
                BlockTokenVariant::Word(word) =>
                    indent!(indent => "WORD #{word}"),
            }
        }
    }

    fn print_invocation(&self, indent: usize, invocation: &Invocation) {
        indent!(indent => "INVOCATION '{}'", invocation.name);
        let indent = indent + 1;
        for argument in &invocation.arguments {
            match &argument {
                DefinitionVariant::Integer(integer) =>
                    self.print_integer_definition(indent, integer),
                DefinitionVariant::Block(block) =>
                    self.print_block_definition(indent, block),
                DefinitionVariant::Reference(reference) =>
                    indent!(indent => "REFERENCE '{}'", reference.name),
            };
        }
    }
}