(* 'top'-like tool for libvirt domains.
   (C) Copyright 2007-2017 Richard W.M. Jones, Red Hat Inc.
   http://libvirt.org/

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*)

open Curses
open Printf

open Opt_gettext.Gettext
open Utils
open Types
open Screen
open Collect

module C = Libvirt.Connect
module D = Libvirt.Domain

(* Keep a historical list of %CPU usages. *)
let historical_cpu = ref []
let historical_cpu_last_time = ref (Unix.gettimeofday ())

(* Redraw the display. *)
let redraw display_mode sort_order
           (_, _, _, _, _, node_info, _, _) (* setup *)
           block_in_bytes
           historical_cpu_delay
           { rd_doms = doms;
             rd_time = time; rd_printable_time = printable_time;
             rd_nr_pcpus = nr_pcpus;
             rd_total_cpu = total_cpu;
             rd_total_cpu_per_pcpu = total_cpu_per_pcpu;
             rd_totals = totals } (* state *)
           pcpu_display =
  clear ();

  (* Get the screen/window size. *)
  let lines, cols = get_size () in

  (* Time. *)
  mvaddstr top_lineno 0 (sprintf "virt-top %s - " printable_time);

  (* Basic node_info. *)
  addstr
    (sprintf "%s %d/%dCPU %dMHz %LdMB "
	     node_info.C.model node_info.C.cpus nr_pcpus node_info.C.mhz
	     (node_info.C.memory /^ 1024L));
  (* Save the cursor position for when we come to draw the
   * historical CPU times (down in this function).
   *)
  let stdscr = stdscr () in
  let historical_cursor = getyx stdscr in

  (match display_mode with

   (*---------- Showing domains ----------*)
   | TaskDisplay ->
      (* Sort domains on current sort_order. *)
      let doms =
	let cmp =
	  match sort_order with
	  | DomainName ->
	     (fun _ -> 0) (* fallthrough to default name compare *)
	  | Processor ->
	     (function
	       | Active rd1, Active rd2 ->
		  compare rd2.rd_percent_cpu rd1.rd_percent_cpu
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | Memory ->
	     (function
	       | Active { rd_info = info1 }, Active { rd_info = info2 } ->
		  compare info2.D.memory info1.D.memory
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | Time ->
	     (function
	       | Active { rd_info = info1 }, Active { rd_info = info2 } ->
		  compare info2.D.cpu_time info1.D.cpu_time
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | DomainID ->
	     (function
	       | Active { rd_domid = id1 }, Active { rd_domid = id2 } ->
		  compare id1 id2
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | NetRX ->
	     (function
	       | Active { rd_net_rx_bytes = r1 }, Active { rd_net_rx_bytes = r2 } ->
		  compare r2 r1
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | NetTX ->
	     (function
	       | Active { rd_net_tx_bytes = r1 }, Active { rd_net_tx_bytes = r2 } ->
		  compare r2 r1
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | BlockRdRq ->
	     (function
	       | Active { rd_block_rd_reqs = r1 }, Active { rd_block_rd_reqs = r2 } ->
		  compare r2 r1
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	  | BlockWrRq ->
	     (function
	       | Active { rd_block_wr_reqs = r1 }, Active { rd_block_wr_reqs = r2 } ->
		  compare r2 r1
	       | Active _, Inactive -> -1
	       | Inactive, Active _ -> 1
	       | Inactive, Inactive -> 0)
	in
	let cmp (name1, dom1) (name2, dom2) =
	  let r = cmp (dom1, dom2) in
	  if r <> 0 then r
	  else compare name1 name2
	in
	List.sort cmp doms in

      (* Print domains. *)
      attron A.reverse;
      let header_string =
        if block_in_bytes
        then "   ID S RDBY WRBY RXBY TXBY %CPU %MEM    TIME   NAME"
        else "   ID S RDRQ WRRQ RXBY TXBY %CPU %MEM    TIME   NAME"
      in
      mvaddstr header_lineno 0
	       (pad cols header_string);
      attroff A.reverse;

      let rec loop lineno = function
	| [] -> ()
	| (name, Active rd) :: doms ->
	   if lineno < lines then (
	     let state = show_state rd.rd_info.D.state in
	     let rd_info =
               if block_in_bytes then Show.int64_option rd.rd_block_rd_bytes
               else Show.int64_option rd.rd_block_rd_reqs in
	     let wr_info =
               if block_in_bytes then Show.int64_option rd.rd_block_wr_bytes
               else Show.int64_option rd.rd_block_wr_reqs in
	     let rx_bytes = Show.int64_option rd.rd_net_rx_bytes in
	     let tx_bytes = Show.int64_option rd.rd_net_tx_bytes in
	     let percent_cpu = Show.percent rd.rd_percent_cpu in
	     let percent_mem = Int64.to_float rd.rd_mem_percent in
	     let percent_mem = Show.percent percent_mem in
	     let time = Show.time rd.rd_info.D.cpu_time in

	     let line =
               sprintf "%5d %c %s %s %s %s %s %s %s %s"
		       rd.rd_domid state rd_info wr_info rx_bytes tx_bytes
		       percent_cpu percent_mem time name in
	     let line = pad cols line in
	     mvaddstr lineno 0 line;
	     loop (lineno+1) doms
	   )
	| (name, Inactive) :: doms -> (* inactive domain *)
	   if lineno < lines then (
	     let line =
	       sprintf
		 "    -                                           (%s)"
		 name in
	     let line = pad cols line in
	     mvaddstr lineno 0 line;
	     loop (lineno+1) doms
	   )
      in
      loop domains_lineno doms

   (*---------- Showing physical CPUs ----------*)
   | PCPUDisplay ->
      let { rd_pcpu_doms = doms;
            rd_pcpu_pcpus = pcpus;
            rd_pcpu_pcpus_cpu_time = pcpus_cpu_time } =
	match pcpu_display with
	| Some p -> p
	| None -> failwith "internal error: no pcpu_display data" in

      (* Display the pCPUs. *)
      let dom_names =
	String.concat "" (
	                List.map (
	                    fun (_, name, _, _, _, _, _, _) ->
		            let len = String.length name in
		            let width = max (len+1) 12 in
		            pad width name
	                  ) doms
	              ) in
      attron A.reverse;
      mvaddstr header_lineno 0 (pad cols ("PHYCPU %CPU " ^ dom_names));
      attroff A.reverse;

      Array.iteri (
	fun p row ->
	  mvaddstr (p+domains_lineno) 0 (sprintf "%4d   " p);
	  let cpu_time = pcpus_cpu_time.(p) in (* ns used on this CPU *)
	  let percent_cpu = 100. *. cpu_time /. total_cpu_per_pcpu in
	  addstr (Show.percent percent_cpu);
	  addch ' ';

	  List.iteri (
	    fun di (domid, name, _, _, _, _, _, _) ->
	      let t = pcpus.(p).(di).(0) in (* hypervisor + domain *)
	      let t_only = pcpus.(p).(di).(1) in (* domain only *)
	      let len = String.length name in
	      let width = max (len+1) 12 in
	      let str_t =
		if t <= 0L then ""
		else (
		  let t = Int64.to_float t in
		  let percent = 100. *. t /. total_cpu_per_pcpu in
		  Show.percent percent
		) in
              let str_t_only =
                if t_only <= 0L then ""
                else (
                  let t_only = Int64.to_float t_only in
                  let percent = 100. *. t_only /. total_cpu_per_pcpu in
                  Show.percent percent
                ) in
              addstr (pad 5 str_t);
              addstr (pad 5 str_t_only);
              addstr (pad (width-10) " ");
	      ()
          ) doms
      ) pcpus;

   (*---------- Showing network interfaces ----------*)
   | NetDisplay ->
      (* Only care about active domains. *)
      let doms =
        List.filter_map (
	    function
	    | (name, Active rd) -> Some (name, rd)
	    | (_, Inactive) -> None
	) doms in

      (* For each domain we have a list of network interfaces seen
       * this slice, and seen in the previous slice, which we now
       * match up to get a list of (domain, interface) for which
       * we have current & previous knowledge.  (And ignore the rest).
       *)
      let devs =
	List.map (
	  fun (name, rd) ->
	    List.filter_map (
	      fun (dev, stats) ->
	        try
		  (* Have prev slice stats for this device? *)
		  let prev_stats =
		    List.assoc dev rd.rd_prev_interface_stats in
		  Some (dev, name, rd, stats, prev_stats)
		with Not_found -> None
	      ) rd.rd_interface_stats
	  ) doms in

      (* Finally we have a list of:
       * device name, domain name, rd_* stuff, curr stats, prev stats.
       *)
      let devs : (string * string * rd_active *
		    D.interface_stats * D.interface_stats) list =
	List.flatten devs in

      (* Difference curr slice & prev slice. *)
      let devs =
        List.map (
	  fun (dev, name, rd, curr, prev) ->
	    dev, name, rd, diff_interface_stats curr prev
	  ) devs in

      (* Sort by current sort order, but map some of the standard
       * sort orders into ones which makes sense here.
       *)
      let devs =
	let cmp =
	  match sort_order with
	  | DomainName ->
	     (fun _ -> 0) (* fallthrough to default name compare *)
	  | DomainID ->
	     (fun (_, { rd_domid = id1 }, _, { rd_domid = id2 }) ->
	      compare id1 id2)
	  | Processor | Memory | Time
          | BlockRdRq | BlockWrRq
	     (* fallthrough to RXBY comparison. *)
	  | NetRX ->
	     (fun ({ D.rx_bytes = b1 }, _, { D.rx_bytes = b2 }, _) ->
	      compare b2 b1)
	  | NetTX ->
	     (fun ({ D.tx_bytes = b1 }, _, { D.tx_bytes = b2 }, _) ->
	      compare b2 b1)
	in
	let cmp (dev1, name1, rd1, stats1) (dev2, name2, rd2, stats2) =
	  let r = cmp (stats1, rd1, stats2, rd2) in
	  if r <> 0 then r
	  else compare (dev1, name1) (dev2, name2)
	in
	List.sort cmp devs in

      (* Print the header for network devices. *)
      attron A.reverse;
      mvaddstr header_lineno 0
	       (pad cols "   ID S RXBY TXBY RXPK TXPK DOMAIN       INTERFACE");
      attroff A.reverse;

      (* Print domains and devices. *)
      let rec loop lineno = function
	| [] -> ()
	| (dev, name, rd, stats) :: devs ->
	   if lineno < lines then (
	     let state = show_state rd.rd_info.D.state in
	     let rx_bytes =
	       if stats.D.rx_bytes >= 0L
	       then Show.int64 stats.D.rx_bytes
	       else "    " in
	     let tx_bytes =
	       if stats.D.tx_bytes >= 0L
	       then Show.int64 stats.D.tx_bytes
	       else "    " in
	     let rx_packets =
	       if stats.D.rx_packets >= 0L
	       then Show.int64 stats.D.rx_packets
	       else "    " in
	     let tx_packets =
	       if stats.D.tx_packets >= 0L
	       then Show.int64 stats.D.tx_packets
	       else "    " in

	     let line = sprintf "%5d %c %s %s %s %s %-12s %s"
		                rd.rd_domid state
		                rx_bytes tx_bytes
		                rx_packets tx_packets
		                (pad 12 name) dev in
	     let line = pad cols line in
	     mvaddstr lineno 0 line;
	     loop (lineno+1) devs
	   )
      in
      loop domains_lineno devs

   (*---------- Showing block devices ----------*)
   | BlockDisplay ->
      (* Only care about active domains. *)
      let doms =
        List.filter_map (
	    function
	    | (name, Active rd) -> Some (name, rd)
	    | (_, Inactive) -> None
	) doms in

      (* For each domain we have a list of block devices seen
       * this slice, and seen in the previous slice, which we now
       * match up to get a list of (domain, device) for which
       * we have current & previous knowledge.  (And ignore the rest).
       *)
      let devs =
	List.map (
	  fun (name, rd) ->
	    List.filter_map (
	      fun (dev, stats) ->
	        try
		  (* Have prev slice stats for this device? *)
		  let prev_stats =
		    List.assoc dev rd.rd_prev_block_stats in
		  Some (dev, name, rd, stats, prev_stats)
		with Not_found -> None
	    ) rd.rd_block_stats
	) doms in

      (* Finally we have a list of:
       * device name, domain name, rd_* stuff, curr stats, prev stats.
       *)
      let devs : (string * string * rd_active *
		    D.block_stats * D.block_stats) list =
	List.flatten devs in

      (* Difference curr slice & prev slice. *)
      let devs =
        List.map (
	  fun (dev, name, rd, curr, prev) ->
	    dev, name, rd, diff_block_stats curr prev
        ) devs in

      (* Sort by current sort order, but map some of the standard
       * sort orders into ones which makes sense here.
       *)
      let devs =
	let cmp =
	  match sort_order with
	  | DomainName ->
	     (fun _ -> 0) (* fallthrough to default name compare *)
	  | DomainID ->
	     (fun (_, { rd_domid = id1 }, _, { rd_domid = id2 }) ->
	      compare id1 id2)
	  | Processor | Memory | Time
          | NetRX | NetTX
	     (* fallthrough to RDRQ comparison. *)
	  | BlockRdRq ->
	     (fun ({ D.rd_req = b1 }, _, { D.rd_req = b2 }, _) ->
	      compare b2 b1)
	  | BlockWrRq ->
	     (fun ({ D.wr_req = b1 }, _, { D.wr_req = b2 }, _) ->
	      compare b2 b1)
	in
	let cmp (dev1, name1, rd1, stats1) (dev2, name2, rd2, stats2) =
	  let r = cmp (stats1, rd1, stats2, rd2) in
	  if r <> 0 then r
	  else compare (dev1, name1) (dev2, name2)
	in
	List.sort cmp devs in

      (* Print the header for block devices. *)
      attron A.reverse;
      mvaddstr header_lineno 0
	       (pad cols "   ID S RDBY WRBY RDRQ WRRQ DOMAIN       DEVICE");
      attroff A.reverse;

      (* Print domains and devices. *)
      let rec loop lineno = function
	| [] -> ()
	| (dev, name, rd, stats) :: devs ->
	   if lineno < lines then (
	     let state = show_state rd.rd_info.D.state in
	     let rd_bytes =
	       if stats.D.rd_bytes >= 0L
	       then Show.int64 stats.D.rd_bytes
	       else "    " in
	     let wr_bytes =
	       if stats.D.wr_bytes >= 0L
	       then Show.int64 stats.D.wr_bytes
	       else "    " in
	     let rd_req =
	       if stats.D.rd_req >= 0L
	       then Show.int64 stats.D.rd_req
	       else "    " in
	     let wr_req =
	       if stats.D.wr_req >= 0L
	       then Show.int64 stats.D.wr_req
	       else "    " in

	     let line = sprintf "%5d %c %s %s %s %s %-12s %s"
		                rd.rd_domid state
		                rd_bytes wr_bytes
		                rd_req wr_req
		                (pad 12 name) dev in
	     let line = pad cols line in
	     mvaddstr lineno 0 line;
	     loop (lineno+1) devs
	   )
      in
      loop domains_lineno devs
  ); (* end of display_mode conditional section *)

  let (count, running, blocked, paused, shutdown, shutoff,
       crashed, active, inactive,
       total_cpu_time, total_memory, total_domU_memory) = totals in

  mvaddstr summary_lineno 0
           (sprintf
	      (f_"%d domains, %d active, %d running, %d sleeping, %d paused, %d inactive D:%d O:%d X:%d")
	      count active running blocked paused inactive shutdown shutoff crashed);

  (* Total %CPU used, and memory summary. *)
  let percent_cpu = 100. *. total_cpu_time /. total_cpu in
  mvaddstr (summary_lineno+1) 0
           (sprintf
	      (f_"CPU: %2.1f%%  Mem: %Ld MB (%Ld MB by guests)")
	      percent_cpu (total_memory /^ 1024L) (total_domU_memory /^ 1024L));

  (* Time to grab another historical %CPU for the list? *)
  if time >= !historical_cpu_last_time +. float historical_cpu_delay
  then (
    historical_cpu := percent_cpu :: list_take 10 !historical_cpu;
    historical_cpu_last_time := time
  );

  (* Display historical CPU time. *)
  let () =
    let y, x = historical_cursor in
    let maxwidth = cols - x in
    let line =
      String.concat " "
	            (List.map (sprintf "%2.1f%%") !historical_cpu) in
    let line = pad maxwidth line in
    mvaddstr y x line;
    () in

  move message_lineno 0; (* Park cursor in message area, as with top. *)
  refresh ()             (* Refresh the display. *)
