mod commit;

use std::{
    env, fmt::Display, fs::File, io::Write, os::unix::process::CommandExt, path::PathBuf,
    process::Command, thread, time::Duration,
};

use ansi_to_tui::IntoText;
use anyhow::Result;
use clap::Parser;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use edtui::{EditorEventHandler, EditorMode, EditorState, EditorTheme, EditorView};
use ratatui::{
    DefaultTerminal, Frame,
    buffer::Buffer,
    layout::{Constraint, Layout, Rect},
    style::{Color, Modifier, Style},
    text::Text,
    widgets::{Block, Clear, List, ListState, StatefulWidget, Widget},
};

use crate::commit::{Breaking, COMMIT_TYPES, CommitType, Message};

#[derive(Debug, Clone)]
pub struct App {
    path: PathBuf,
    exit: bool,
    mode: Mode,
    message: Message,
    focus: Field,
    status: String,
    log: String,
    diff: String,
}

#[derive(Clone, Default)]
pub struct AppState {
    editor: EditorState,
    list: ListState,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum Mode {
    Main,
    Edit,
    ConfirmQuit,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum Field {
    Type,
    Scope,
    Description,
    Content,
    Breaking,
    Footer,
    Preview,
}

impl From<Field> for Color {
    fn from(value: Field) -> Self {
        match value {
            Field::Type => Color::Magenta,
            Field::Scope => Color::Green,
            Field::Description => Color::Cyan,
            Field::Content => Color::Blue,
            Field::Breaking => Color::Red,
            Field::Footer => Color::Yellow,
            Field::Preview => Color::White,
        }
    }
}

impl Field {
    fn next(self) -> Self {
        match self {
            Field::Type => Field::Scope,
            Field::Scope => Field::Description,
            Field::Description => Field::Content,
            Field::Content => Field::Breaking,
            Field::Breaking => Field::Footer,
            Field::Footer => Field::Preview,
            Field::Preview => Field::Preview,
        }
    }

    fn prev(self) -> Self {
        match self {
            Field::Type => Field::Type,
            Field::Scope => Field::Type,
            Field::Description => Field::Scope,
            Field::Content => Field::Description,
            Field::Breaking => Field::Content,
            Field::Footer => Field::Breaking,
            Field::Preview => Field::Footer,
        }
    }
}

impl Display for Field {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Field::Type => "Type",
            Field::Scope => "Scope",
            Field::Description => "Description",
            Field::Content => "Content",
            Field::Breaking => "Breaking",
            Field::Footer => "Footer",
            Field::Preview => "Preview",
        })
    }
}

impl App {
    pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<()> {
        let mut state = AppState::default();
        let mut event_handler = EditorEventHandler::vim_mode();
        while !self.exit {
            terminal.draw(|frame| self.draw(frame, &mut state))?;
            self.handle_events(&mut event_handler, &mut state)?;
        }
        Ok(())
    }

    fn draw(&self, frame: &mut Frame, state: &mut AppState) {
        frame.render_stateful_widget(self, frame.area(), state);
    }

    fn handle_events(
        &mut self,
        event_handler: &mut EditorEventHandler,
        state: &mut AppState,
    ) -> Result<()> {
        match event::read()? {
            Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
                self.handle_key_event(key_event, event_handler, state)
            }
            _ => {}
        };
        Ok(())
    }

    fn get_field(&self, field: Field) -> String {
        match field {
            Field::Type => self.message.ctype.to_string(),
            Field::Scope => self.message.scope.clone().unwrap_or_default(),
            Field::Description => self.message.description.clone(),
            Field::Content => self.message.body.clone(),
            Field::Breaking => self.message.breaking.0.join("\n"),
            Field::Footer => self.message.footer.to_string(),
            Field::Preview => self.message.to_string(),
        }
    }

    fn set_field(&mut self, field: Field, content: String) -> Result<()> {
        match field {
            Field::Type => self.message.ctype = content.parse()?,
            Field::Scope => {
                self.message.scope = if content.is_empty() {
                    None
                } else {
                    Some(content)
                }
            }
            Field::Description => {
                self.message.description =
                    content.lines().next().unwrap_or_default().trim().parse()?
            }
            Field::Content => self.message.body = content.parse()?,
            Field::Breaking => {
                self.message.breaking = Breaking(
                    content
                        .trim()
                        .lines()
                        .map(|x| x.trim().to_string())
                        .collect(),
                );
                self.message.is_breaking |= !self.message.breaking.0.is_empty();
            }
            Field::Footer => self.message.footer = content.parse()?,
            Field::Preview => self.message = content.parse()?,
        }
        Ok(())
    }

    fn check_ok(&mut self) -> bool {
        if self.message.ctype == CommitType::None {
            self.status = "Empty type".to_string();
            return false;
        }
        if self.message.description.is_empty() {
            self.status = "Empty description".to_string();
            return false;
        }
        true
    }

    fn save(&self, empty: bool) -> Result<()> {
        let mut file = File::create(&self.path)?;
        if !empty {
            write!(file, "{}", self.message)?;
        }
        Ok(())
    }

    fn edit(&mut self, state: &mut AppState) {
        if self.focus == Field::Type {
            state
                .list
                .select(COMMIT_TYPES.iter().position(|x| *x == self.message.ctype));
            self.mode = Mode::Edit;
        } else {
            state.editor = EditorState::default();
            state.editor.lines = self.get_field(self.focus).into();
            state.editor.mode = EditorMode::Insert;
            self.mode = Mode::Edit;
        }
    }

    fn handle_key_event(
        &mut self,
        key_event: KeyEvent,
        event_handler: &mut EditorEventHandler,
        state: &mut AppState,
    ) {
        match &self.mode {
            Mode::Edit if self.focus == Field::Type => {
                match key_event.code {
                    KeyCode::Char('q') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
                        self.mode = Mode::Main;
                    }
                    KeyCode::Enter => {
                        if let Some(i) = state.list.selected() {
                            self.message.ctype = COMMIT_TYPES[i];
                        }
                        self.mode = Mode::Main;
                    }
                    KeyCode::Char('s') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
                        if let Some(i) = state.list.selected() {
                            self.message.ctype = COMMIT_TYPES[i];
                        }
                        self.mode = Mode::Main;
                    }
                    KeyCode::Char('f') => {
                        if state.list.selected() == Some(CommitType::Feature.ind()) {
                            state.list.select(Some(CommitType::Fix.ind()));
                        } else {
                            state.list.select(Some(CommitType::Feature.ind()));
                        }
                    }
                    KeyCode::Char('c') => {
                        if state.list.selected() == Some(CommitType::Chore.ind()) {
                            state.list.select(Some(CommitType::Ci.ind()));
                        } else {
                            state.list.select(Some(CommitType::Chore.ind()));
                        }
                    }
                    KeyCode::Char('b') => state.list.select(Some(CommitType::Build.ind())),
                    KeyCode::Char('d') => state.list.select(Some(CommitType::Docs.ind())),
                    KeyCode::Char('s') => state.list.select(Some(CommitType::Style.ind())),
                    KeyCode::Char('r') => state.list.select(Some(CommitType::Refactor.ind())),
                    KeyCode::Char('p') => state.list.select(Some(CommitType::Perf.ind())),
                    KeyCode::Char('t') => state.list.select(Some(CommitType::Test.ind())),
                    KeyCode::Char('n') => state.list.select(Some(CommitType::None.ind())),
                    KeyCode::Up | KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('j') => {
                        state.list.select_previous();
                    }
                    KeyCode::Down | KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('k') => {
                        state.list.select_next();
                    }
                    _ => {}
                }

                event_handler.on_key_event(key_event, &mut state.editor);
            }
            Mode::Edit => {
                match key_event.code {
                    KeyCode::Char('q') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
                        self.mode = Mode::Main;
                    }
                    KeyCode::Char('s') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
                        if let Err(e) = self.set_field(self.focus, state.editor.lines.to_string()) {
                            self.status = e.to_string();
                        };
                        self.mode = Mode::Main;
                    }
                    _ => {}
                }

                event_handler.on_key_event(key_event, &mut state.editor);
            }
            Mode::Main => match key_event.code {
                KeyCode::Char('q') => self.mode = Mode::ConfirmQuit,
                KeyCode::Char('t') => {
                    self.focus = Field::Type;
                    self.edit(state);
                }
                KeyCode::Char('s') => {
                    self.focus = Field::Scope;
                    self.edit(state);
                }
                KeyCode::Char('d') => {
                    self.focus = Field::Description;
                    self.edit(state);
                }
                KeyCode::Char('c') => {
                    self.focus = Field::Content;
                    self.edit(state);
                }
                KeyCode::Char('b') => {
                    self.focus = Field::Breaking;
                    self.edit(state);
                }
                KeyCode::Char('f') => {
                    self.focus = Field::Footer;
                    self.edit(state);
                }
                KeyCode::Char('p') => {
                    self.focus = Field::Preview;
                    self.edit(state);
                }
                KeyCode::Char('1') | KeyCode::Char('!') => {
                    self.message.is_breaking = !self.message.is_breaking
                }
                KeyCode::Up | KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('j') => {
                    self.focus = self.focus.next();
                }
                KeyCode::Down | KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('k') => {
                    self.focus = self.focus.prev();
                }
                KeyCode::Enter => {
                    self.edit(state);
                }
                _ => {}
            },
            Mode::ConfirmQuit => match key_event.code {
                KeyCode::Char('y') => {
                    if !self.check_ok() {
                        self.mode = Mode::Main;
                        return;
                    }
                    if let Err(e) = self.save(false) {
                        self.status = e.to_string();
                    } else {
                        self.exit()
                    }
                }
                KeyCode::Char('n') => self.exit(),
                KeyCode::Char('c') | KeyCode::Esc => self.mode = Mode::Main,
                KeyCode::Char('a') => {
                    if let Err(e) = self.save(true) {
                        self.status = e.to_string();
                    } else {
                        self.exit()
                    }
                }
                _ => {}
            },
        }
    }

    fn exit(&mut self) {
        self.exit = true;
    }

    fn block_area_custom<S: AsRef<str>>(
        &self,
        outer: Rect,
        buf: &mut Buffer,
        title: S,
        color: Color,
        highlight: bool,
    ) -> Rect {
        let mut style = Style::reset().fg(color);
        if highlight {
            style = style.add_modifier(Modifier::BOLD);
        }
        let block = Block::bordered().title(title.as_ref()).style(style);
        let area = block.inner(outer);
        block.render(outer, buf);
        area
    }

    fn block_area(&self, outer: Rect, buf: &mut Buffer, field: Field) -> Rect {
        let highlight = self.focus == field;
        let field_name = field.to_string();
        let title_parts = field_name.split_at(1);
        let title = format!("({}){}", title_parts.0, title_parts.1);
        self.block_area_custom(outer, buf, title, field.into(), highlight)
    }

    fn update_git_state(&mut self) -> Result<()> {
        let output = Command::new("git")
            .args(["log", "--color=always"])
            .output()?;
        self.log = String::from_utf8(output.stdout)?;
        let output = Command::new("git")
            .args(["diff", "--cached", "--color=always"])
            .output()?;
        self.diff = String::from_utf8(output.stdout)?;
        Ok(())
    }
}

impl StatefulWidget for &App {
    type State = AppState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
        let [left, right] =
            Layout::horizontal([Constraint::Fill(2), Constraint::Fill(1)]).areas(area);
        let vert: [_; 6] = Layout::vertical([
            Constraint::Length(3),
            Constraint::Fill(1),
            Constraint::Fill(1),
            Constraint::Fill(1),
            Constraint::Fill(2),
            Constraint::Length(1),
        ])
        .areas(left);
        let vertr: [_; 2] =
            Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(right);
        let top_areas: [_; 3] = Layout::horizontal([
            Constraint::Length(10),
            Constraint::Length(10),
            Constraint::Fill(1),
        ])
        .areas(vert[0]);

        let type_area = self.block_area(top_areas[0], buf, Field::Type);
        let scope_area = self.block_area(top_areas[1], buf, Field::Scope);
        let description_area = self.block_area(top_areas[2], buf, Field::Description);
        let content_area = self.block_area(vert[1], buf, Field::Content);
        let breaking_area = self.block_area(vert[2], buf, Field::Breaking);
        let footer_area = self.block_area(vert[3], buf, Field::Footer);
        let preview_area = self.block_area(vert[4], buf, Field::Preview);
        let log_area = self.block_area_custom(vertr[0], buf, "Log", Color::Gray, false);
        let diff_area = self.block_area_custom(vertr[1], buf, "Diff", Color::Gray, false);

        Text::from(self.message.ctype.to_string())
            .style(Style::default().fg(Color::White))
            .render(type_area, buf);
        Text::from(self.message.scope.clone().unwrap_or_default())
            .style(Style::default().fg(Color::White))
            .render(scope_area, buf);
        Text::from(self.message.description.as_str())
            .style(Style::default().fg(Color::White))
            .render(description_area, buf);
        Text::from(self.message.body.as_str())
            .style(Style::default().fg(Color::White))
            .render(content_area, buf);
        Text::from(self.message.breaking.to_string())
            .style(Style::default().fg(Color::White))
            .render(breaking_area, buf);
        Text::from(self.message.footer.to_string())
            .style(Style::default().fg(Color::White))
            .render(footer_area, buf);
        Text::from(self.message.to_string().replace("\t", "    "))
            .style(Style::default().fg(Color::White))
            .render(preview_area, buf);
        self.log
            .as_str()
            .into_text()
            .unwrap_or_default()
            .style(Style::default().fg(Color::Gray))
            .render(log_area, buf);
        self.diff
            .as_str()
            .into_text()
            .unwrap_or_default()
            .style(Style::default().fg(Color::Gray))
            .render(diff_area, buf);

        let mut status = match &self.mode {
            Mode::Edit if self.focus == Field::Type => {
                "ctrl+s: save | ctrl+q: close | arrow/hjkl: focus"
            }
            Mode::Edit => "ctrl+s: save | ctrl+q: close",
            Mode::Main => "q: quit | enter: edit | !/1: toggle breaking | arrow/hjkl: focus",
            Mode::ConfirmQuit => "y: yes | n: no | esc: cancel",
        }
        .to_string();
        if !self.status.is_empty() {
            status += &format!(" | {}", self.status);
        }
        Text::from(status).render(vert[5], buf);

        match &self.mode {
            Mode::Main => {}
            Mode::Edit if self.focus == Field::Type => {
                let height = 2 + COMMIT_TYPES.len() as u16;
                let overlay_area = Rect {
                    x: area.width / 4,
                    y: (area.height - height) / 2,
                    width: area.width / 2,
                    height,
                };
                Clear.render(overlay_area, buf);
                let list_area = self.block_area_custom(
                    overlay_area,
                    buf,
                    self.focus.to_string(),
                    self.focus.into(),
                    false,
                );
                StatefulWidget::render(
                    List::new(COMMIT_TYPES.iter().map(|x| {
                        let mut name = x.to_string();
                        if name.is_empty() {
                            name = "none".to_string();
                        }
                        let (head, tail) = name.split_at(1);
                        format!("({head}){tail}")
                    }))
                    .highlight_style(Style::default().bg(Field::Type.into()).fg(Color::White))
                    .highlight_symbol("▶ "),
                    list_area,
                    buf,
                    &mut state.list,
                );
            }
            Mode::Edit => {
                let single_line = self.focus == Field::Scope || self.focus == Field::Description;
                let height = if single_line { 4 } else { area.height / 2 };
                let overlay_area = Rect {
                    x: area.width / 4,
                    y: (area.height - height) / 2,
                    width: area.width / 2,
                    height,
                };
                Clear.render(overlay_area, buf);
                let edit_area = self.block_area_custom(
                    overlay_area,
                    buf,
                    self.focus.to_string(),
                    self.focus.into(),
                    false,
                );
                EditorView::new(&mut state.editor)
                    .theme(EditorTheme::default())
                    .wrap(true)
                    .tab_width(2)
                    .single_line(single_line)
                    .render(edit_area, buf);
            }
            Mode::ConfirmQuit => {
                let overlay_area = Rect {
                    x: area.width / 4,
                    y: (area.height.max(5) - 4) / 2,
                    width: area.width / 2,
                    height: 4,
                };
                Clear.render(overlay_area, buf);
                let confirm_area =
                    self.block_area_custom(overlay_area, buf, "Quitting", Color::Red, true);
                Text::from("Save?\n(y)es, (n)o, (c)ancel, (a)bort commit")
                    .centered()
                    .render(confirm_area, buf);
            }
        }
    }
}

#[derive(Parser)]
struct Cli {
    file: PathBuf,
}

fn main_res(args: &Cli) -> Result<()> {
    let content = std::fs::read_to_string(&args.file)?;
    let message = content.parse()?;
    let mut app = App {
        path: args.file.clone(),
        exit: false,
        message,
        status: String::new(),
        focus: Field::Type,
        mode: Mode::Main,
        log: String::new(),
        diff: String::new(),
    };
    app.update_git_state()?;

    ratatui::run(|terminal| app.run(terminal))?;
    Ok(())
}

fn main() {
    let args = Cli::parse();

    if let Err(e) = main_res(&args) {
        eprintln!("{e}");
        thread::sleep(Duration::from_secs(2));
        let fallback = env::var("EDITOR").unwrap_or("vim".to_string());
        eprintln!("{}", Command::new(fallback).arg(args.file).exec());
    }
}
