use crate::models::error::{ModelError, ModelErrorKind};
use crate::models::{ArticleID, FeedID, Url};
use crate::schema::articles;
use chrono::NaiveDateTime;
use diesel::backend::Backend;
use diesel::deserialize;
use diesel::deserialize::FromSql;
use diesel::serialize;
use diesel::serialize::{Output, ToSql};
use diesel::sql_types::Integer;
use diesel::sqlite::Sqlite;
use failure::ResultExt;
use serde_derive::{Deserialize, Serialize};
use std;
use std::io::Write;
use std::path::PathBuf;

#[derive(Identifiable, Insertable, Queryable, Clone, PartialEq, Debug)]
#[primary_key(article_id)]
#[table_name = "articles"]
pub struct Article {
    pub article_id: ArticleID,
    pub title: Option<String>,
    pub author: Option<String>,
    pub feed_id: FeedID,
    pub url: Option<Url>,
    #[column_name = "timestamp"]
    pub date: NaiveDateTime,
    pub summary: Option<String>,
    pub direction: Option<Direction>,
    pub unread: Read,
    pub marked: Marked,
}

impl Article {
    #[allow(clippy::type_complexity)]
    pub fn decompose(
        self,
    ) -> (
        ArticleID,
        Option<String>,
        Option<String>,
        FeedID,
        Option<Url>,
        NaiveDateTime,
        Option<String>,
        Option<Direction>,
        Read,
        Marked,
    ) {
        (
            self.article_id,
            self.title,
            self.author,
            self.feed_id,
            self.url,
            self.date,
            self.summary,
            self.direction,
            self.unread,
            self.marked,
        )
    }
}

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

#[derive(Identifiable, Insertable, Queryable, Clone, PartialEq, Debug)]
#[primary_key(article_id)]
#[table_name = "articles"]
pub struct FatArticle {
    pub article_id: ArticleID,
    pub title: Option<String>,
    pub author: Option<String>,
    pub feed_id: FeedID,
    pub url: Option<Url>,
    #[column_name = "timestamp"]
    pub date: NaiveDateTime,
    pub html: Option<String>,
    pub summary: Option<String>,
    pub direction: Option<Direction>,
    pub unread: Read,
    pub marked: Marked,
    pub scraped_content: Option<String>,
    pub plain_text: Option<String>,
}

impl FatArticle {
    pub fn save_html(&self, path: &PathBuf) -> Result<(), ModelError> {
        if let Some(ref html) = self.html {
            if let Ok(()) = std::fs::create_dir_all(&path) {
                let mut file_name = match self.title.clone() {
                    Some(file_name) => file_name,
                    None => "Unknown Title".to_owned(),
                };
                file_name.push_str(".html");
                let path = path.join(file_name);
                let mut html_file = std::fs::File::create(&path).context(ModelErrorKind::IO)?;
                html_file.write_all(html.as_bytes()).context(ModelErrorKind::IO)?;
            }
        }

        Err(ModelErrorKind::IO)?
    }

    #[allow(clippy::type_complexity)]
    pub fn decompose(
        self,
    ) -> (
        ArticleID,
        Option<String>,
        Option<String>,
        FeedID,
        Option<Url>,
        NaiveDateTime,
        Option<String>,
        Option<String>,
        Option<Direction>,
        Read,
        Marked,
        Option<String>,
        Option<String>,
    ) {
        (
            self.article_id,
            self.title,
            self.author,
            self.feed_id,
            self.url,
            self.date,
            self.html,
            self.summary,
            self.direction,
            self.unread,
            self.marked,
            self.scraped_content,
            self.plain_text,
        )
    }
}

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

#[derive(PartialEq, Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)]
#[sql_type = "Integer"]
pub enum Direction {
    LeftToRight,
    RightToLeft,
}

impl Direction {
    pub fn to_int(self) -> i32 {
        self as i32
    }

    pub fn from_int(int: i32) -> Self {
        match int {
            0 => Direction::LeftToRight,
            1 => Direction::RightToLeft,

            _ => Direction::LeftToRight,
        }
    }
}

impl FromSql<Integer, Sqlite> for Direction {
    fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
        let int = not_none!(bytes).read_integer();
        Ok(Direction::from_int(int))
    }
}

impl ToSql<Integer, Sqlite> for Direction {
    fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Sqlite>) -> serialize::Result {
        ToSql::<Integer, Sqlite>::to_sql(&self.to_int(), out)
    }
}

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

#[derive(PartialEq, Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)]
#[sql_type = "Integer"]
pub enum Read {
    Read,
    Unread,
}

impl Read {
    pub fn to_int(self) -> i32 {
        self as i32
    }

    pub fn from_int(int: i32) -> Self {
        match int {
            0 => Read::Read,
            1 => Read::Unread,

            _ => Read::Read,
        }
    }

    pub fn invert(&self) -> Read {
        match self {
            Read::Read => Read::Unread,
            Read::Unread => Read::Read,
        }
    }
}

impl FromSql<Integer, Sqlite> for Read {
    fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
        let int = not_none!(bytes).read_integer();
        Ok(Read::from_int(int))
    }
}

impl ToSql<Integer, Sqlite> for Read {
    fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Sqlite>) -> serialize::Result {
        ToSql::<Integer, Sqlite>::to_sql(&self.to_int(), out)
    }
}

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

#[derive(PartialEq, Copy, Clone, Debug, AsExpression, FromSqlRow, Serialize, Deserialize)]
#[sql_type = "Integer"]
pub enum Marked {
    Marked,
    Unmarked,
}

impl Marked {
    pub fn to_int(self) -> i32 {
        self as i32
    }

    pub fn from_int(int: i32) -> Self {
        match int {
            0 => Marked::Marked,
            1 => Marked::Unmarked,

            _ => Marked::Unmarked,
        }
    }

    pub fn invert(&self) -> Marked {
        match self {
            Marked::Marked => Marked::Unmarked,
            Marked::Unmarked => Marked::Marked,
        }
    }
}

impl FromSql<Integer, Sqlite> for Marked {
    fn from_sql(bytes: Option<&<Sqlite as Backend>::RawValue>) -> deserialize::Result<Self> {
        let int = not_none!(bytes).read_integer();
        Ok(Marked::from_int(int))
    }
}

impl ToSql<Integer, Sqlite> for Marked {
    fn to_sql<W: Write>(&self, out: &mut Output<'_, W, Sqlite>) -> serialize::Result {
        ToSql::<Integer, Sqlite>::to_sql(&self.to_int(), out)
    }
}

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

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ArticleOrder {
    NewestFirst,
    OldestFirst,
}

impl ArticleOrder {
    pub fn invert(&self) -> ArticleOrder {
        match self {
            ArticleOrder::NewestFirst => ArticleOrder::OldestFirst,
            ArticleOrder::OldestFirst => ArticleOrder::NewestFirst,
        }
    }

    pub fn to_str(&self) -> &str {
        match self {
            ArticleOrder::NewestFirst => "Newest First",
            ArticleOrder::OldestFirst => "Oldest First",
        }
    }
}
