// Written by Radim Kolar <hsn@sendmail.cz> 2025
//
// SPDX-License-Identifier: Unlicense OR CC0-1.0

#![forbid(unsafe_code)]

fn do_help() {
   println!("cat [ options ] file ...");
   println!("Concatenate files to standard output");

   println!("  -h | -? Display help");
}

/*      M A I N      */

const CAT_OPTSTRING: &str = "h?";

fn main() {
   use getopt3::hideBin;
   let g = getopt3::new(hideBin(std::env::args()), CAT_OPTSTRING).expect("Invalid optstring");
   if g.has('h') || g.has('?') {
      // display help and exit
      do_help();
      std::process::exit(0);
   } else {
      // standard cat operation printing files
      let mut anyerror = false;
      if g.iter().all(|s| s == "-") {
         // argument list empty or containing only "-"
         // cat operation mode: piping stdin to stdout
         match display_data(std::io::stdin()) {
            Ok(_) => {}
            Err(IoDirectionError::Read(e)) => {
               perror("stdin read error", &e);
               anyerror = true;
            }
            Err(IoDirectionError::Write(e)) => {
               perror("write error", &e);
               anyerror = true;
            }
         }
      } else {
         // we have some files to display
         for f in g {
            match if f.eq("-") {
               // - means use stdin
               Ok(Box::new(std::io::stdin()) as Box<dyn std::io::Read>)
            } else if f.eq(":") {
               // : means empty file
               Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Empty input"))
            } else {
               // process f as file name
               std::fs::File::open(&f).map(|file| Box::new(file) as Box<dyn std::io::Read>)
            } {
               // file open OK
               Ok(file) => match display_data(file) {
                  Ok(_) => {}
                  Err(IoDirectionError::Write(e)) => {
                     perror("write error", &e);
                     anyerror = true;
                  }
                  Err(IoDirectionError::Read(e)) => {
                     perror(&format!("Can't read '{}'", &f), &e);
                     anyerror = true;
                  }
               },
               // file open ERR
               Err(e) => match e.kind() {
                  // special case - empty file or input stream ended
                  std::io::ErrorKind::UnexpectedEof => {}
                  _ => {
                     perror(&format!("Can't open '{}'", &f), &e);
                     anyerror = true;
                  }
               },
            }
         }
      }
      if anyerror {
         std::process::exit(1)
      } else {
         std::process::exit(0)
      }
   }
}

fn perror(context: &str, err: &std::io::Error) {
   eprintln!("cat: {}: {}", context, err);
}

/* buffer sizes */
const READ_BUFFER: usize = 2_usize.pow(16);
const WRITE_BUFFER: usize = 2_usize.pow(16);

#[derive(Debug)]
enum IoDirectionError {
   Read(std::io::Error),
   Write(std::io::Error),
}

/**
  Displays data to stdout.

  ### Returns
  io::Error and direction
*/
fn display_data(source: impl std::io::Read) -> Result<(), IoDirectionError> {
   let mut reader = std::io::BufReader::with_capacity(READ_BUFFER, source);
   let stdout_std = std::io::stdout();
   let stdout_lock = stdout_std.lock();
   let mut stdout = std::io::BufWriter::with_capacity(WRITE_BUFFER, stdout_lock);
   let mut buffer = [0; READ_BUFFER];

   loop {
      let bytes_read = match (&mut reader as &mut dyn std::io::Read).read(&mut buffer) {
         Ok(0) => break, // EOF on read
         Ok(n) => n,     // read OK
         Err(e) => {
            // read ERR
            // flush stdout if error happens during flush return it
            // instead of read error because it appeared sooner
            return match (&mut stdout as &mut dyn std::io::Write).flush() {
               Ok(()) => Err(IoDirectionError::Read(e)),
               Err(ew) => Err(IoDirectionError::Write(ew)),
            };
         }
      };

      if let Err(e) = (&mut stdout as &mut dyn std::io::Write).write_all(&buffer[..bytes_read]) {
         return Err(IoDirectionError::Write(e));
      }
   }

   match (&mut stdout as &mut dyn std::io::Write).flush() {
      Ok(()) => Ok(()),
      Err(e) => Err(IoDirectionError::Write(e)),
   }
}

#[cfg(test)]
mod tests {

   /**
     Check if iter().all() returns true for empty input
   */
   #[test]
   fn test_prazdny_iterator() {
      let prazdny_vec: Vec<String> = Vec::new();
      let mut iter_prazdny = prazdny_vec.iter();

      assert_eq!(
         iter_prazdny.all(|s| s == "nonexistent"),
         true,
         "Should return true for empty iterator."
      );
   }
}
