use std::borrow::Cow;
use std::fmt::Write as _;
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};

use log::{info, log_enabled, warn};
use terminal_size::terminal_size;
use unicode_width::UnicodeWidthStr;
use yansi::{Paint, Style};

use crate::config::{Config, OptionStyle};
use crate::error::{Error, ErrorKind, Result};
use crate::util::PagePathExt;

const TITLE: &str = "# ";
const DESC: &str = "> ";
const BULLET: &str = "- ";
const EXAMPLE: char = '`';

struct RenderStyles {
    title: Style,
    desc: Style,
    bullet: Style,
    example: Style,
    url: Style,
    inline_code: Style,
    placeholder: Style,
}

pub struct PageRenderer<'a> {
    /// Path to the page.
    path: &'a Path,
    /// A buffered reader containing the page.
    reader: BufReader<File>,
    /// A buffered handle to standard output.
    stdout: BufWriter<io::StdoutLock<'static>>,
    /// The line of the page that is currently being worked with.
    current_line: String,
    /// The line number of the current line.
    lnum: usize,
    /// Max line length.
    max_len: Option<usize>,
    /// Style configuration.
    style: RenderStyles,
    /// Other options.
    cfg: &'a Config,
}

/// Write a `yansi::Painted` to a `String`.
///
/// This is used to append something to a `String` without creating `String`s for every part of a
/// line that's highlighted using a different style.
macro_rules! write_paint {
    ($buf:expr, $what:expr) => {
        // This will never return an error, we're writing to a `String`.
        let _ = write!($buf, "{}", $what);
    };
}

/// Type of the line.
/// Does not include types that aren't wrapped (i.e. title, empty lines and examples).
#[derive(Clone, Copy, PartialEq)]
enum LineType {
    Desc,
    Bullet,
}

impl<'a> PageRenderer<'a> {
    fn hl_code(&self, s: &str, style_normal: Style) -> String {
        let split: Vec<&str> = s.split('`').collect();
        let mut buf = String::with_capacity(s.len());

        // Highlight beginning not found.
        if split.len() == 1 {
            write_paint!(buf, s.paint(style_normal));
            return buf;
        }

        for (i, part) in split.into_iter().enumerate() {
            // Only odd indexes contain the part to be highlighted.
            // "aa `bb` cc `dd` ee"
            // 0: "aa "
            // 1: "bb"      (highlighted)
            // 2: " cc "
            // 3: "dd"      (highlighted)
            // 4: " ee"
            if i % 2 == 0 {
                write_paint!(buf, part.paint(style_normal));
            } else {
                write_paint!(buf, part.paint(self.style.inline_code));
            }
        }

        buf
    }

    fn hl_url(&self, s: &str, style_normal: Style) -> String {
        let split: Vec<&str> = s.split("<http").collect();
        let mut buf = String::with_capacity(s.len());

        // Highlight beginning not found.
        if split.len() == 1 {
            write_paint!(buf, s.paint(style_normal));
            return buf;
        }

        for part in split {
            // The first part of the second split contains the part to be highlighted.
            //
            // "More information: <https://example.com>."
            // 0: "More information: " => does not match
            // 1: "s://example.com>."  => 0: "s://example.com" (highlighted)
            //                            1: ">."
            if let Some((inside, outside)) = part.split_once('>') {
                // "<http" is used to detect URLs. It must be added back.
                let hl = format!("http{inside}");
                write_paint!(buf, hl.paint(self.style.url));
                write_paint!(buf, outside.paint(style_normal));
            } else {
                // Highlight ending not found.
                write_paint!(buf, part.paint(style_normal));
            }
        }

        buf
    }

    fn hl_placeholder(&self, s: &str, style_normal: Style) -> String {
        let split: Vec<&str> = s.split("{{").collect();
        let mut buf = String::with_capacity(s.len());

        // Highlight beginning not found.
        if split.len() == 1 {
            write_paint!(buf, s.paint(style_normal));
            return buf;
        }

        for part in split {
            // Finding the last double ending brace is required for special cases with three
            // closing curly braces ("}}}").
            // The first brace is inside the placeholder, and the last two mark the end of it.
            //
            // The first part of the second split contains the part to be highlighted.
            //
            // "aa bb {{cc}} {{dd}} ee"
            // 0: "aa bb "   => does not match
            // 1: "cc}} "    => 0: "cc"    (highlighted)
            //                  1: " "
            // 2: "dd}} ee"  => 0: "dd"    (highlighted)
            //                  1: " ee"
            if let Some((inside, outside)) = part.rsplit_once("}}") {
                // Select the long or short option.
                // Skip if the user wants to display both or if the placeholder doesn't contain
                // option selection (`[-s|--long]`).
                if self.cfg.output.option_style != OptionStyle::Both
                    && inside.starts_with('[')
                    && inside.ends_with(']')
                    && let Some((short, long)) = inside.split_once('|')
                {
                    // A single option will be displayed, using the normal style (static part).
                    if self.cfg.output.option_style == OptionStyle::Short {
                        // Cut out the leading `[`.
                        write_paint!(buf, &short[1..].paint(style_normal));
                    } else {
                        // Cut out the trailing `]`.
                        write_paint!(buf, &long[..long.len() - 1].paint(style_normal));
                    }
                } else {
                    // Both options will be displayed, or this isn't an option placeholder.
                    // The placeholder style is used in both cases.
                    write_paint!(buf, inside.paint(self.style.placeholder));
                }

                write_paint!(buf, outside.paint(style_normal));
            } else {
                // Highlight ending not found.
                write_paint!(buf, part.paint(style_normal));
            }
        }

        buf
    }

    /// Split the line into multiple lines if it's longer than the configured max length.
    fn splitln(&self, s: &'a str, indent: &str, ltype: LineType) -> Cow<'a, str> {
        let Some(max_len) = self.max_len else {
            // We don't have the max length. Just print the entire line then.
            return Cow::Borrowed(s);
        };

        let len_indent = indent.len();
        let prefix_width = if ltype == LineType::Bullet && self.cfg.output.show_hyphens {
            self.cfg.output.example_prefix.width()
        } else {
            0
        };
        let base_width = len_indent + prefix_width;

        if base_width + s.width() <= max_len {
            // The line is shorter than the max length. There is nothing to do.
            return Cow::Borrowed(s);
        }

        let words = s.split(' ');
        let len_s = s.len();
        let mut cur_width = base_width;
        //                  current_len + base_width * amount of added newlines
        let mut buf = String::with_capacity(len_s + base_width * (len_s / max_len));

        // If the example prefix is set, we need more whitespace at the beginning of the next line.
        let indent = if prefix_width == 0 {
            Cow::Borrowed(indent)
        } else {
            Cow::Owned(" ".repeat(prefix_width) + indent)
        };

        // Is the current word highlighted (inside backticks)?
        let mut inside_hl = false;

        let style_normal = match ltype {
            LineType::Desc => self.style.desc,
            LineType::Bullet => self.style.bullet,
        };

        for w in words {
            let mut w_width = w.width();
            let backtick_count = w.chars().filter(|x| *x == '`').count();
            // Backticks won't be displayed.
            w_width -= backtick_count;

            // current + word + space after the word
            if cur_width + w_width + 1 > max_len && cur_width != base_width {
                // If the next word is added, the line will be longer than the configured line
                // length.
                //
                // We need to add a newline + indentation, and reset the current length.
                if yansi::is_enabled() {
                    // Style reset. Without this, whitespace will have a background color (if one
                    // is set).
                    let _ = style_normal.fmt_suffix(&mut buf);
                }
                buf.push('\n');
                buf += &indent;
                if yansi::is_enabled() {
                    // Reenable the style.
                    let _ = if inside_hl {
                        self.style.inline_code.fmt_prefix(&mut buf)
                    } else {
                        style_normal.fmt_prefix(&mut buf)
                    };
                }
                cur_width = base_width;
            } else if cur_width != base_width {
                // If this isn't the beginning of the line, add a space after the word.
                buf.push(' ');
                cur_width += 1;
            }

            buf += w;
            cur_width += w_width;

            // If there are two backticks in `w`, then `w` contains all the highlighted text.
            // If there is only one (e.g `ab cd` => split into words ["`ab", "cd`"]), it
            // starts/ends the highlight.
            if backtick_count == 1 {
                inside_hl = !inside_hl;
            }
        }

        Cow::Owned(buf)
    }

    /// Print or render the page according to the provided config.
    pub fn print(path: &'a Path, cfg: &'a Config) -> Result<()> {
        let mut page = File::open(path)
            .map_err(|e| Error::new(format!("'{}': {e}", path.display())).kind(ErrorKind::Io))?;

        if cfg.output.raw_markdown {
            io::copy(&mut page, &mut io::stdout()).map_err(|e| {
                Error::new(format!("'{}': {e}", path.display())).kind(ErrorKind::Io)
            })?;
            return Ok(());
        }

        Self {
            path,
            reader: BufReader::new(page),
            stdout: BufWriter::new(io::stdout().lock()),
            current_line: String::new(),
            lnum: 0,
            max_len: if cfg.output.line_length == 0 {
                terminal_size().map(|x| x.0.0 as usize)
            } else {
                Some(cfg.output.line_length)
            },
            style: RenderStyles {
                title: cfg.style.title.into(),
                desc: cfg.style.description.into(),
                bullet: cfg.style.bullet.into(),
                example: cfg.style.example.into(),
                url: cfg.style.url.into(),
                inline_code: cfg.style.inline_code.into(),
                placeholder: cfg.style.placeholder.into(),
            },
            cfg,
        }
        .render()
    }

    /// Print the first page that was found and warnings for every other page.
    pub fn print_cache_result(paths: &'a [PathBuf], cfg: &'a Config) -> Result<()> {
        if log_enabled!(log::Level::Warn) && paths.len() != 1 {
            let mut stderr = io::stderr().lock();
            let other_pages = &paths[1..];
            let width = other_pages
                .iter()
                .map(|x| x.page_platform().unwrap().len())
                .max()
                .unwrap();

            warn!("{} page(s) found for other platforms:", other_pages.len());

            for (i, path) in other_pages.iter().enumerate() {
                // The path always ends with the page file, and its parent is always the
                // platform directory. This is safe to unwrap.
                let name = path.page_name().unwrap();
                let platform = path.page_platform().unwrap();

                writeln!(
                    stderr,
                    "{} {platform:<width$} (tldr --platform {platform} {name})",
                    format!("{}.", i + 1).green().bold(),
                )?;
            }
        }

        // This is safe to unwrap - errors would have already been catched in run().
        let first = paths.first().unwrap();

        if cfg.output.edit_link {
            // The path always starts with the cache dir.
            let mut components = first.strip_prefix(&cfg.cache.dir).unwrap().iter();

            // pages.lang/platform/filename.md
            let pages_dir = components.next().unwrap().to_string_lossy();
            let pages_dir = match &*pages_dir {
                // There is a `pages.en` symlink, but GitHub doesn't resolve it.
                "pages.en" => "pages",
                _ => &pages_dir,
            };

            let platform = components.next().unwrap().display();
            let filename = components.next().unwrap().display();

            info!(
                "edit this page on GitHub:\nhttps://github.com/tldr-pages/tldr/edit/main/{pages_dir}/{platform}/{filename}"
            );
        }

        Self::print(first, cfg)
    }

    /// Load the next line into the line buffer.
    fn next_line(&mut self) -> Result<usize> {
        // The `Paint` trait from yansi also has a method named `clear`.
        // This will be resolved in a future release: https://github.com/SergioBenitez/yansi/issues/42
        //self.current_line.clear();
        String::clear(&mut self.current_line);
        self.lnum += 1;
        let n = self
            .reader
            .read_line(&mut self.current_line)
            .map_err(|e| Error::new(format!("'{}': {e}", self.path.display())))?;
        let len = self.current_line.trim_end().len();
        self.current_line.truncate(len);

        Ok(n)
    }

    /// Write the current line to the page buffer as a title.
    fn add_title(&mut self) -> Result<()> {
        if !self.cfg.output.show_title {
            return Ok(());
        }
        self.add_newline()?;

        let line = self.current_line.strip_prefix(TITLE).unwrap();
        let title = if self.cfg.output.platform_title
            && let Some(platform) = self.path.page_platform()
        {
            Cow::Owned(format!("{platform}/{line}"))
        } else {
            Cow::Borrowed(line)
        };

        let title = title.paint(self.style.title);
        let indent = " ".repeat(self.cfg.indent.title);
        writeln!(self.stdout, "{indent}{title}")?;

        Ok(())
    }

    /// Write the current line to the page buffer as a description.
    fn add_desc(&mut self) -> Result<()> {
        let indent = " ".repeat(self.cfg.indent.description);
        let line = self.current_line.strip_prefix(DESC).unwrap();
        let line = self.splitln(line, &indent, LineType::Desc);
        let desc = self.hl_code(&self.hl_url(&line, self.style.desc), self.style.desc);

        writeln!(self.stdout, "{indent}{desc}")?;

        Ok(())
    }

    /// Write the current line to the page buffer as a bullet point.
    fn add_bullet(&mut self) -> Result<()> {
        let indent = " ".repeat(self.cfg.indent.bullet);
        let line = self.current_line.strip_prefix(BULLET).unwrap();
        let line = self.splitln(line, &indent, LineType::Bullet);

        let bullet = self.hl_code(&self.hl_url(&line, self.style.bullet), self.style.bullet);
        write!(self.stdout, "{indent}")?;
        if self.cfg.output.show_hyphens {
            let prefix = self.cfg.output.example_prefix.paint(self.style.bullet);
            write!(self.stdout, "{prefix}")?;
        }
        writeln!(self.stdout, "{bullet}")?;

        Ok(())
    }

    /// Write the current line to the page buffer as an example.
    fn add_example(&mut self) -> Result<()> {
        // Add spaces around escaped curly braces in order not to
        // interpret them as a placeholder (e.g. in "\{\{{{ }}\}\}").
        self.current_line = self
            .current_line
            .replace("\\{\\{", " \\{\\{ ")
            .replace("\\}\\}", " \\}\\} ");

        let indent = " ".repeat(self.cfg.indent.example);
        let line = self
            .current_line
            .strip_prefix(EXAMPLE)
            .unwrap()
            .strip_suffix('`')
            .ok_or_else(|| {
                Error::parse_page(self.path, self.lnum, &self.current_line)
                    .describe("\nEvery line with an example must end with a backtick '`'.")
            })?;

        let example = self
            .hl_placeholder(line, self.style.example)
            // Remove the extra spaces and backslashes.
            .replace(" \\{\\{ ", "{{")
            .replace(" \\}\\} ", "}}");

        writeln!(self.stdout, "{indent}{example}")?;

        Ok(())
    }

    /// Write a newline to the page buffer if compact mode is not turned on.
    fn add_newline(&mut self) -> Result<()> {
        if !self.cfg.output.compact {
            writeln!(self.stdout)?;
        }

        Ok(())
    }

    /// Render the page to standard output.
    fn render(&mut self) -> Result<()> {
        while self.next_line()? != 0 {
            if self.current_line.starts_with(TITLE) {
                self.add_title()?;
            } else if self.current_line.starts_with(DESC) {
                self.add_desc()?;
            } else if self.current_line.starts_with(BULLET) {
                self.add_bullet()?;
            } else if self.current_line.starts_with(EXAMPLE) {
                self.add_example()?;
            } else if self.current_line.chars().all(char::is_whitespace) {
                self.add_newline()?;
            } else {
                return Err(
                    Error::parse_page(self.path, self.lnum, &self.current_line).describe(
                        "\nEvery non-empty line must begin with either '# ', '> ', '- ' or '`'.",
                    ),
                );
            }
        }

        self.add_newline()?;
        Ok(self.stdout.flush()?)
    }
}
