// Copyright 2025 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Progress bar for CLI operations with multiple update modes.

use std::{
    io::{stdout, Write},
    time::{Duration, Instant},
};

/// Progress tracker with configurable update intervals.
pub struct Progress {
    /// Time when tracking started.
    start_time: Instant,
    /// Time when tracking finished.
    end_time: Instant,
    /// Time of last progress update.
    last_tick_time: Instant,
    /// Total number of items to process.
    pub total: usize,
    /// Current number of items processed.
    current: usize,
    /// Configuration controlling the interval at which new updates are printed.
    interval: ProgressInterval,
    /// Text prefix for progress messages.
    prefix: String,
    /// Whether to auto-print on updates.
    autoprint: bool,
    /// Whether processing has completed.
    finished: bool,
    /// Whether to print updates on the same terminal line.
    same_line: bool,
    /// Maximum width for the visual bar portion. None means no cap.
    max_bar_width: Option<usize>,
    /// Character count of the last printed message, used to clear reflowed lines.
    last_printed_len: usize,
    /// Cached terminal width, set once per print() call.
    cached_term_width: Option<usize>,
    /// Whether msg() truncates output to terminal width.
    truncate: bool,
}

/// Defines the "refresh rate" of the progress updates, by defining either a time span between
/// updates, a number of discrete updates required between updates, or a percentage of the total
/// number of items.
pub enum ProgressInterval {
    /// The Time interval causes an update to be printed every N units of time.
    Time(Duration),
    /// The Count interval causes an update to be printed when the count has been incremented
    /// N times.
    Count(usize),
    /// The Percent interval causes an update to be printed when the current count has
    /// incremented by N percent of the total count.
    Percent(usize),
}

impl ProgressInterval {
    /// Return true if an interval boundary has been reached.
    fn should_update(&self, progress: &Progress) -> bool {
        match self {
            ProgressInterval::Time(interval) => {
                let now = Instant::now();
                &now.duration_since(progress.last_tick_time) > interval
            }
            ProgressInterval::Count(interval) => progress.current % interval == 0,
            ProgressInterval::Percent(interval) => {
                let current_bucket = (100 * progress.current) / progress.total / interval;
                let prev_bucket =
                    (100 * progress.current.saturating_sub(1)) / progress.total / interval;
                current_bucket != prev_bucket
            }
        }
    }
}

impl Progress {
    /// Creates a new Progress tracker with the given total and update interval.
    pub fn new(total: usize, interval: ProgressInterval) -> Progress {
        let now = Instant::now();
        // Percent(0) causes division by zero in should_update.
        let interval = match interval {
            ProgressInterval::Percent(0) => ProgressInterval::Percent(1),
            other => other,
        };

        Progress {
            start_time: now,
            end_time: now, // will be overwritten later
            total,
            current: 0,
            interval,
            prefix: "".to_string(),
            finished: false,
            last_tick_time: now,
            autoprint: true,
            same_line: true,
            max_bar_width: Some(40),
            last_printed_len: 0,
            cached_term_width: None,
            truncate: true,
        }
    }
    /// Sets the text prefix displayed before progress info.
    pub fn set_prefix(&mut self, msg: String) {
        self.prefix = msg;
    }
    /// true causes Progress to print to stdout, false causes it to not print.
    pub fn set_autoprint(&mut self, enabled: bool) {
        self.autoprint = enabled;
    }

    /// Print updates on the same line in the terminal, as a typical progress bar would.  In some
    /// CI environments (like AWS CodeBuild) this should be disabled in order for progress updates
    /// to appear.
    pub fn set_same_line(&mut self, same_line: bool) {
        self.same_line = same_line;
    }

    /// Sets the maximum width for the visual bar. None removes the cap.
    pub fn set_max_bar_width(&mut self, width: Option<usize>) {
        self.max_bar_width = width;
    }

    /// Controls whether msg() truncates output to terminal width. Defaults to true.
    pub fn set_truncate(&mut self, truncate: bool) {
        self.truncate = truncate;
    }

    /// Assign a value to the current amount.
    /// Returns `true` if a new line should be printed based on the chosen interval.
    pub fn set(&mut self, new_current: usize) -> bool {
        let mut should_print = false;

        if !self.finished {
            let prev = self.current;
            self.current = new_current;
            should_print = self.post_update(prev);
        }

        should_print
    }

    /// Increment the current count by 1.
    /// Returns `true` if a new line should be printed based on the chosen interval.
    pub fn inc(&mut self) -> bool {
        self.set(self.current + 1)
    }

    /// Handle tasks after the current count has been changed, such as updating the latest
    /// timestamp.  Returns `true` if the post_update step determines that a new item should be
    /// printed based on the chosen interval.
    fn post_update(&mut self, prev: usize) -> bool {
        let mut should_print = false;

        if self.should_update(prev) {
            self.last_tick_time = Instant::now();
            should_print = true;
            if self.autoprint {
                self.print();
            }
        }

        if self.current >= self.total {
            self.finished = true;
            self.end_time = Instant::now();
            if self.autoprint {
                println!("{}", self.finish_msg());
            }
        }
        should_print
    }

    /// Get the current count.
    pub fn current_count(&self) -> usize {
        self.current
    }

    /// Returns true if a progress update should be logged (either printed to the screen if
    /// `autoprint` is enabled, or returned by `msg()`).  A variety of conditions determine when
    /// updates are logged, including the total count being reached, or the user-selected interval
    /// reaching a boundary.  For example, if the user chose an interval of
    /// `Time(Duration::from_secs(2))` then an elapse of 2 seconds represents a boundary of that
    /// interval, and an update will be logged.
    fn should_update(&self, prev_count: usize) -> bool {
        let still_running = !self.finished;
        let just_finished = self.current >= self.total;
        let on_interval_boundary = self.interval.should_update(self);
        let just_started = prev_count == 0;

        still_running && (just_started || just_finished || on_interval_boundary)
    }

    /// Returns the terminal width, using the cached value if available.
    fn term_width(&self) -> usize {
        self.cached_term_width.unwrap_or_else(|| {
            termion::terminal_size()
                .map(|(w, _)| w as usize)
                .unwrap_or(80)
                .max(1)
        })
    }

    /// Prints the current progress message to stdout.
    fn print(&mut self) {
        if self.same_line {
            self.cached_term_width = Some(
                termion::terminal_size()
                    .map(|(w, _)| w as usize)
                    .unwrap_or(80)
                    .max(1),
            );
            let msg = self.msg();
            let term_width = self.term_width();

            if self.last_printed_len > term_width {
                let extra_lines = self.last_printed_len.saturating_sub(1) / term_width;
                print!("\x1b[{extra_lines}A");
            }

            print!(
                "\r{}{}{}",
                termion::clear::CurrentLine,
                msg,
                termion::clear::AfterCursor
            );
            stdout().flush().expect("stdout flush failed");
            self.last_printed_len = msg.chars().count();
            self.cached_term_width = None;
        } else {
            println!("{}", self.msg());
        }
    }

    /// Get the current progress message.
    pub fn msg(&self) -> String {
        if self.total == 0 {
            return format!("{}0/0 0%", self.prefix);
        }

        let complete_char = "\u{2588}";
        let incomplete_char = "\u{2591}";

        let percent = (100.0 * self.current as f64 / self.total as f64) as usize;

        let current_pad =
            self.total
                .checked_ilog10()
                .unwrap_or(0)
                .saturating_sub(self.current.checked_ilog10().unwrap_or(0)) as usize;

        let elapsed_str =
            humantime::format_duration(Duration::from_secs(self.start_time.elapsed().as_secs()))
                .to_string();
        let eta_str = humantime::format_duration(self.time_estimate()).to_string();
        let hz = self.current as f32 / self.start_time.elapsed().as_secs_f32();
        let hz_str = format!("{hz:.2}");

        let without_bar = format!(
            "{msg}{current_pad}{current}/{total} {percent:>3}% ({elapsed} elapsed, {eta} remaining, {hz}/s)",
            msg = self.prefix,
            current_pad = " ".repeat(current_pad),
            current = self.current,
            total = self.total,
            percent = percent,
            elapsed = elapsed_str,
            eta = eta_str,
            hz = hz_str,
        );

        let term_width = self.term_width();

        const MIN_BAR_WIDTH: usize = 4;
        const BAR_OVERHEAD: usize = 4;

        let non_bar_len = without_bar.chars().count();

        let result = if term_width > non_bar_len + BAR_OVERHEAD + MIN_BAR_WIDTH {
            let available = term_width - non_bar_len - BAR_OVERHEAD;
            let bar_width = match self.max_bar_width {
                Some(max) => available.min(max),
                None => available,
            };
            let filled = (percent * bar_width) / 100;
            let bar_section = format!(
                ": {}{} ",
                complete_char.repeat(filled),
                incomplete_char.repeat(bar_width - filled),
            );
            format!(
                "{msg}{current_pad}{current}/{total}{bar_section}{percent:>3}% ({elapsed} elapsed, {eta} remaining, {hz}/s)",
                msg = self.prefix,
                current_pad = " ".repeat(current_pad),
                current = self.current,
                total = self.total,
                bar_section = bar_section,
                percent = percent,
                elapsed = elapsed_str,
                eta = eta_str,
                hz = hz_str,
            )
        } else {
            without_bar
        };

        if self.truncate {
            result.chars().take(term_width.saturating_sub(1)).collect()
        } else {
            result
        }
    }

    /// Returns the completion message with total elapsed time.
    pub fn finish_msg(&self) -> String {
        format!(
            "{}{msg}finished in {dur}",
            if self.same_line { "\n" } else { "" },
            msg = self.prefix,
            dur = humantime::format_duration(Duration::from_secs(
                self.end_time.duration_since(self.start_time).as_secs()
            )),
        )
    }

    /// Returns an estimate of the remaining time.
    fn time_estimate(&self) -> Duration {
        let remaining_count = self.total - self.current;
        let elapsed_secs = self.start_time.elapsed().as_secs_f64();

        Duration::from_secs((remaining_count as f64 * elapsed_secs / self.current as f64) as u64)
    }
}

#[cfg(test)]
mod tests {

    use std::thread;

    use super::*;

    #[test]
    fn over_increment() {
        // increment too many times
        let mut pb = Progress::new(4, ProgressInterval::Count(2));
        for _ in 0..100 {
            pb.inc();
        }
    }

    #[test]
    fn count_interval_test() {
        let mut pb = Progress::new(10, ProgressInterval::Count(2));
        let mut msgs = Vec::new();
        for _ in 0..10 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }
        assert_eq!(msgs.len(), 6);
    }

    #[test]
    fn percent_interval_test() {
        let mut pb = Progress::new(10, ProgressInterval::Percent(50));

        let mut msgs = Vec::new();

        for _ in 0..10 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        assert_eq!(msgs.len(), 3);
    }

    #[test]
    fn percent_interval_every_item() {
        let mut pb = Progress::new(10, ProgressInterval::Percent(10));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..10 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        assert_eq!(msgs.len(), 10);
    }

    #[test]
    fn percent_interval_quarter() {
        let mut pb = Progress::new(100, ProgressInterval::Percent(25));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..100 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        assert_eq!(msgs.len(), 5);
    }

    #[test]
    fn percent_interval_start_and_finish_only() {
        let mut pb = Progress::new(5, ProgressInterval::Percent(100));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..5 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        assert_eq!(msgs.len(), 2);
    }

    #[test]
    fn percent_interval_every_percent() {
        let mut pb = Progress::new(100, ProgressInterval::Percent(1));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..100 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        assert_eq!(msgs.len(), 100);
    }

    #[test]
    fn percent_interval_one_percent_large_total() {
        let mut pb = Progress::new(1000, ProgressInterval::Percent(1));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..1000 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        // start + 99 boundary crossings (1%..99%) + finish = 101
        assert_eq!(msgs.len(), 101);
    }

    #[test]
    fn percent_interval_one_percent_very_large_total() {
        let mut pb = Progress::new(10000, ProgressInterval::Percent(1));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..10000 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        // start + 99 boundary crossings (1%..99%) + finish = 101
        assert_eq!(msgs.len(), 101);
    }

    #[test]
    fn percent_interval_non_divisible() {
        let mut pb = Progress::new(10, ProgressInterval::Percent(33));
        pb.set_autoprint(false);

        let mut msgs = Vec::new();
        for _ in 0..10 {
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        // start + crossing 33% + crossing 66% + finish = 4
        assert_eq!(msgs.len(), 4);
    }

    #[test]
    fn time_interval_test() {
        let mut pb = Progress::new(8, ProgressInterval::Time(Duration::from_millis(50)));

        let mut msgs = Vec::new();

        for _ in 0..8 {
            thread::sleep(Duration::from_millis(30));
            if pb.inc() {
                msgs.push(pb.msg());
            }
        }

        assert_eq!(msgs.len(), 5);
    }
}
