use indexmap::IndexMap;
use schematic_types::{
    ArrayType, BooleanType, EnumType, FloatType, IntegerType, LiteralType, ObjectType, Schema,
    SchemaType, StringType, StructType, TupleType, UnionType,
};
use thiserror::Error;

/// Error returned from schema rendering and generation.
#[derive(Debug, Error)]
pub enum RenderError {
    /// I/O error while writing the rendered output.
    #[error(transparent)]
    Io(#[from] std::io::Error),

    /// Catch-all rendering failure with a free-form message.
    #[error("{0}")]
    Custom(String),
}

impl RenderError {
    /// Construct a [`RenderError::Custom`] from any displayable value.
    pub fn custom<T: std::fmt::Display>(message: T) -> Self {
        Self::Custom(message.to_string())
    }
}

pub type RenderResult<T = String> = std::result::Result<T, RenderError>;

/// Renders [`SchemaType`]s to a distinct format (derived from generic `O`) for
/// use within a [`SchemaGenerator`].
///
/// [`SchemaGenerator`]: crate::schema::SchemaGenerator
pub trait SchemaRenderer<O = String> {
    /// Return true of the provided name is a referenced type.
    fn is_reference(&self, name: &str) -> bool;

    /// Render an [`ArrayType`] to a string.
    fn render_array(&mut self, array: &ArrayType, schema: &Schema) -> RenderResult<O>;

    /// Render a boolean type to a string.
    fn render_boolean(&mut self, boolean: &BooleanType, schema: &Schema) -> RenderResult<O>;

    /// Render an [`EnumType`] to a string.
    fn render_enum(&mut self, enu: &EnumType, schema: &Schema) -> RenderResult<O>;

    /// Render a [`FloatType`] to a string.
    fn render_float(&mut self, float: &FloatType, schema: &Schema) -> RenderResult<O>;

    /// Render an [`IntegerType`] to a string.
    fn render_integer(&mut self, integer: &IntegerType, schema: &Schema) -> RenderResult<O>;

    /// Render a [`LiteralType`] to a string.
    fn render_literal(&mut self, literal: &LiteralType, schema: &Schema) -> RenderResult<O>;

    /// Render a null type to a string.
    fn render_null(&mut self, schema: &Schema) -> RenderResult<O>;

    /// Render an [`ObjectType`] to a string.
    fn render_object(&mut self, object: &ObjectType, schema: &Schema) -> RenderResult<O>;

    /// Render a referenced type to a string.
    fn render_reference(&mut self, reference: &str, schema: &Schema) -> RenderResult<O>;

    /// Render a [`StringType`] to a string.
    fn render_string(&mut self, string: &StringType, schema: &Schema) -> RenderResult<O>;

    /// Render a [`StructType`] to a string.
    fn render_struct(&mut self, structure: &StructType, schema: &Schema) -> RenderResult<O>;

    /// Render a [`TupleType`] to a string.
    fn render_tuple(&mut self, tuple: &TupleType, schema: &Schema) -> RenderResult<O>;

    /// Render a [`UnionType`] to a string.
    fn render_union(&mut self, uni: &UnionType, schema: &Schema) -> RenderResult<O>;

    /// Render an unknown type to a string.
    fn render_unknown(&mut self, schema: &Schema) -> RenderResult<O>;

    /// Render all possible variants of the provided [`Schema`] to a string.
    /// If a variant has an explicit name, and that name is a reference, return
    /// the name instead of rendering the type.
    fn render_schema(&mut self, schema: &Schema) -> RenderResult<O> {
        if let Some(name) = &schema.name
            && self.is_reference(name)
        {
            return self.render_reference(name, schema);
        }

        self.render_schema_without_reference(schema)
    }

    /// Like [`render_schema`] but does not check for references.
    ///
    /// [`render_schema`]: Self::render_schema
    fn render_schema_without_reference(&mut self, schema: &Schema) -> RenderResult<O> {
        match &schema.ty {
            SchemaType::Null => self.render_null(schema),
            SchemaType::Unknown => self.render_unknown(schema),
            SchemaType::Array(array) => self.render_array(array, schema),
            SchemaType::Boolean(boolean) => self.render_boolean(boolean, schema),
            SchemaType::Enum(enu) => self.render_enum(enu, schema),
            SchemaType::Float(float) => self.render_float(float, schema),
            SchemaType::Integer(integer) => self.render_integer(integer, schema),
            SchemaType::Literal(literal) => self.render_literal(literal, schema),
            SchemaType::Object(object) => self.render_object(object, schema),
            SchemaType::Struct(structure) => self.render_struct(structure, schema),
            SchemaType::String(string) => self.render_string(string, schema),
            SchemaType::Tuple(tuple) => self.render_tuple(tuple, schema),
            SchemaType::Union(uni) => self.render_union(uni, schema),
            SchemaType::Reference(reference) => self.render_reference(&reference.name, schema),
        }
    }

    /// Render the list of [`Schema`]s to a string, in the order they are
    /// listed.
    /// References between types can be resolved using the provided `schemas`
    /// map.
    fn render(&mut self, schemas: IndexMap<String, Schema>) -> RenderResult;
}
