/*
This tool is part of the WhiteboxTools geospatial analysis library.
Authors: Dr. John Lindsay
Created: 24/09/2017
Last Modified: 04/01/2019
License: MIT
*/

use whitebox_lidar::*;
use whitebox_raster::*;
use whitebox_common::structures::{Array2D, DistanceMetric, FixedRadiusSearch3D, Point3D};
use crate::tools::*;
use std::env;
use std::f64;
use std::fs::File;
use std::io::prelude::*;
use std::io::{Error, ErrorKind};
use std::path;
use std::path::Path;
use std::process::Command;

/// This tool performs a kappa index of agreement (KIA) analysis on the classification values of two LiDAR
/// (LAS) files. The output report HTML file should be displayed automatically but can also be displayed
/// afterwards in any web browser. As a measure of overall classification accuracy, the KIA is more robust
/// than the percent agreement calculation because it takes into account the agreement occurring by random
/// chance. In addition to the KIA, the tool will output the producer's and user's accuracy, the overall
/// accuracy, and the error matrix. The KIA is often used as a means of assessing the accuracy of an image
/// classification analysis; however the `LidarKappaIndex` tool performs the analysis on a point-to-point
/// basis, comparing the class values of the points in one input LAS file with the corresponding nearest
/// points in the second input LAS file.
///
/// The user must also specify the name and resolution of an output raster file, which is used to show the
/// spatial distribution of class accuracy. Each grid cell contains the overall accuracy, i.e. the points
/// correctly classified divided by the total number of points contained within the cell, expressed as a
/// percentage.
pub struct LidarKappaIndex {
    name: String,
    description: String,
    toolbox: String,
    parameters: Vec<ToolParameter>,
    example_usage: String,
}

impl LidarKappaIndex {
    pub fn new() -> LidarKappaIndex {
        // public constructor
        let name = "LidarKappaIndex".to_string();
        let toolbox = "LiDAR Tools".to_string();
        let description = "Performs a kappa index of agreement (KIA) analysis on the classifications of two LAS files.".to_string();

        let mut parameters = vec![];
        parameters.push(ToolParameter {
            name: "Input LiDAR File (Classification)".to_owned(),
            flags: vec!["--i1".to_owned(), "--input1".to_owned()],
            description: "Input LiDAR classification file.".to_owned(),
            parameter_type: ParameterType::ExistingFile(ParameterFileType::Lidar),
            default_value: None,
            optional: false,
        });

        parameters.push(ToolParameter {
            name: "Input LiDAR File (Reference)".to_owned(),
            flags: vec!["--i2".to_owned(), "--input2".to_owned()],
            description: "Input LiDAR reference file.".to_owned(),
            parameter_type: ParameterType::ExistingFile(ParameterFileType::Lidar),
            default_value: None,
            optional: false,
        });

        parameters.push(ToolParameter {
            name: "Output HTML File".to_owned(),
            flags: vec!["-o".to_owned(), "--output".to_owned()],
            description: "Output HTML file.".to_owned(),
            parameter_type: ParameterType::NewFile(ParameterFileType::Html),
            default_value: None,
            optional: false,
        });

        parameters.push(ToolParameter {
            name: "Output Class Accuracy Raster File".to_owned(),
            flags: vec!["--class_accuracy".to_owned()],
            description: "Output classification accuracy raster file.".to_owned(),
            parameter_type: ParameterType::NewFile(ParameterFileType::Raster),
            default_value: None,
            optional: false,
        });

        parameters.push(ToolParameter {
            name: "Grid Resolution".to_owned(),
            flags: vec!["--resolution".to_owned()],
            description: "Output raster's grid resolution.".to_owned(),
            parameter_type: ParameterType::Float,
            default_value: Some("1.0".to_owned()),
            optional: true,
        });

        let sep: String = path::MAIN_SEPARATOR.to_string();
        let p = format!("{}", env::current_dir().unwrap().display());
        let e = format!("{}", env::current_exe().unwrap().display());
        let mut short_exe = e
            .replace(&p, "")
            .replace(".exe", "")
            .replace(".", "")
            .replace(&sep, "");
        if e.contains(".exe") {
            short_exe += ".exe";
        }
        let usage = format!(">>.*{0} -r={1} -v --wd=\"*path*to*data*\" --i1=class.las --i2=reference.las -o=kia.html --class_accuracy=new_file.tif --resolution=0.5", short_exe, name).replace("*", &sep);

        LidarKappaIndex {
            name: name,
            description: description,
            toolbox: toolbox,
            parameters: parameters,
            example_usage: usage,
        }
    }
}

impl WhiteboxTool for LidarKappaIndex {
    fn get_source_file(&self) -> String {
        String::from(file!())
    }

    fn get_tool_name(&self) -> String {
        self.name.clone()
    }

    fn get_tool_description(&self) -> String {
        self.description.clone()
    }

    fn get_tool_parameters(&self) -> String {
        let mut s = String::from("{\"parameters\": [");
        for i in 0..self.parameters.len() {
            if i < self.parameters.len() - 1 {
                s.push_str(&(self.parameters[i].to_string()));
                s.push_str(",");
            } else {
                s.push_str(&(self.parameters[i].to_string()));
            }
        }
        s.push_str("]}");
        s
    }

    fn get_example_usage(&self) -> String {
        self.example_usage.clone()
    }

    fn get_toolbox(&self) -> String {
        self.toolbox.clone()
    }

    fn run<'a>(
        &self,
        args: Vec<String>,
        working_directory: &'a str,
        verbose: bool,
    ) -> Result<(), Error> {
        let mut input_file1 = String::new();
        let mut input_file2 = String::new();
        let mut output_file = String::new();
        let mut class_accuracy_file = String::new();
        let mut grid_res: f64 = 1.0;

        if args.len() == 0 {
            return Err(Error::new(
                ErrorKind::InvalidInput,
                "Tool run with no parameters.",
            ));
        }
        for i in 0..args.len() {
            let mut arg = args[i].replace("\"", "");
            arg = arg.replace("\'", "");
            let cmd = arg.split("="); // in case an equals sign was used
            let vec = cmd.collect::<Vec<&str>>();
            let mut keyval = false;
            if vec.len() > 1 {
                keyval = true;
            }
            let flag_val = vec[0].to_lowercase().replace("--", "-");
            if flag_val == "-i1" || flag_val == "-input1" {
                input_file1 = if keyval {
                    vec[1].to_string()
                } else {
                    args[i + 1].to_string()
                };
            } else if flag_val == "-i2" || flag_val == "-input2" {
                input_file2 = if keyval {
                    vec[1].to_string()
                } else {
                    args[i + 1].to_string()
                };
            } else if flag_val == "-o" || flag_val == "-output" {
                output_file = if keyval {
                    vec[1].to_string()
                } else {
                    args[i + 1].to_string()
                };
            } else if flag_val.contains("-class_ac") {
                class_accuracy_file = if keyval {
                    vec[1].to_string()
                } else {
                    args[i + 1].to_string()
                };
            } else if flag_val == "-resolution" {
                grid_res = if keyval {
                    vec[1]
                        .to_string()
                        .parse::<f64>()
                        .expect(&format!("Error parsing {}", flag_val))
                } else {
                    args[i + 1]
                        .to_string()
                        .parse::<f64>()
                        .expect(&format!("Error parsing {}", flag_val))
                };
            }
        }

        if verbose {
            println!("***************{}", "*".repeat(self.get_tool_name().len()));
            println!("* Welcome to {} *", self.get_tool_name());
            println!("***************{}", "*".repeat(self.get_tool_name().len()));
        }

        let sep: String = path::MAIN_SEPARATOR.to_string();

        let mut progress: i32;
        let mut old_progress: i32 = -1;

        if !input_file1.contains(&sep) && !input_file1.contains("/") {
            input_file1 = format!("{}{}", working_directory, input_file1);
        }
        if !input_file2.contains(&sep) && !input_file2.contains("/") {
            input_file2 = format!("{}{}", working_directory, input_file2);
        }
        if !output_file.contains(&sep) && !output_file.contains("/") {
            output_file = format!("{}{}", working_directory, output_file);
        }
        if !output_file.ends_with(".html") {
            output_file = output_file + ".html";
        }
        if !class_accuracy_file.contains(&sep) && !class_accuracy_file.contains("/") {
            class_accuracy_file = format!("{}{}", working_directory, class_accuracy_file);
        }

        if verbose {
            println!("Reading data...")
        };
        let start = Instant::now();

        let input1: LasFile = match LasFile::new(&input_file1, "r") {
            Ok(lf) => lf,
            Err(err) => panic!("Error: {}", err),
        };

        let input2: LasFile = match LasFile::new(&input_file2, "r") {
            Ok(lf) => lf,
            Err(err) => panic!("Error: {}", err),
        };

        let num_points1 = input1.header.number_of_points;
        let num_points2 = input2.header.number_of_points;

        // create the output raster
        let west: f64 = input1.header.min_x;
        let north: f64 = input1.header.max_y;
        let rows: usize = (((north - input1.header.min_y) / grid_res).ceil()) as usize;
        let columns: usize = (((input1.header.max_x - west) / grid_res).ceil()) as usize;
        let south: f64 = north - rows as f64 * grid_res;
        let east = west + columns as f64 * grid_res;
        let nodata = -32768.0f64;
        let half_grid_res = grid_res / 2.0;
        let ns_range = north - south;
        let ew_range = east - west;

        let mut configs = RasterConfigs {
            ..Default::default()
        };
        configs.rows = rows as usize;
        configs.columns = columns as usize;
        configs.north = north;
        configs.south = south;
        configs.east = east;
        configs.west = west;
        configs.resolution_x = grid_res;
        configs.resolution_y = grid_res;
        configs.nodata = nodata;
        configs.data_type = DataType::F32;
        configs.photometric_interp = PhotometricInterpretation::Continuous;

        let mut class_accuracy = Raster::initialize_using_config(&class_accuracy_file, &configs);
        class_accuracy.reinitialize_values(0f64);

        // Create an Array2D to store the number of points in each grid cell
        let mut num_points_cell: Array2D<u32> =
            Array2D::new(rows as isize, columns as isize, 0u32, u32::max_value())?;

        // Place all the input1 points into a FixedRadiusSearch3D
        let mut p1: Point3D;
        // let mut p2: Point3D;
        let mut pd1: PointData;
        let mut pd2: PointData;
        let (mut row, mut col): (isize, isize);
        let mut frs: FixedRadiusSearch3D<usize> =
            FixedRadiusSearch3D::new(0.1f64, DistanceMetric::SquaredEuclidean);
        for i in 0..num_points2 as usize {
            // p1 = input2.get_point_info(i);
            p1 = input2.get_transformed_coords(i);
            frs.insert(p1.x, p1.y, p1.z, i);
            if verbose {
                progress = (100.0_f64 * i as f64 / num_points2 as f64) as i32;
                if progress != old_progress {
                    println!("Binning points: {}%", progress);
                    old_progress = progress;
                }
            }
        }

        let mut error_matrix: [[usize; 256]; 256] = [[0; 256]; 256];
        let mut active_class: [bool; 256] = [false; 256];
        let (mut class1, mut class2): (usize, usize);
        for i in 0..num_points1 as usize {
            pd1 = input1.get_point_info(i);
            p1 = input1.get_transformed_coords(i);
            // find the nearest point in input2 to p1
            let ret = frs.knn_search(p1.x, p1.y, p1.z, 1);
            if ret.len() > 0 {
                pd2 = input2.get_point_info(ret[0].0);
                // p2 = input2.get_transformed_coords(ret[0].0);
                class1 = pd1.classification() as usize;
                class2 = pd2.classification() as usize;
                error_matrix[class1][class2] += 1;
                active_class[class1] = true;
                active_class[class2] = true;

                col = (((columns - 1) as f64 * (p1.x - west - half_grid_res) / ew_range).round())
                    as isize;
                row = (((rows - 1) as f64 * (north - half_grid_res - p1.y) / ns_range).round())
                    as isize;

                num_points_cell.increment(row, col, 1);
                if class1 == class2 {
                    class_accuracy.increment(row, col, 1f64);
                }
            }

            if verbose {
                progress = (100.0_f64 * i as f64 / num_points1 as f64) as i32;
                if progress != old_progress {
                    println!("Progress: {}%", progress);
                    old_progress = progress;
                }
            }
        }

        let mut z: f64;
        let mut n: u32;
        for row in 0..rows as isize {
            for col in 0..columns as isize {
                n = num_points_cell.get_value(row, col);
                if n > 0u32 {
                    z = class_accuracy.get_value(row, col);
                    class_accuracy.set_value(row, col, 100.0 * z / n as f64);
                } else {
                    class_accuracy.set_value(row, col, nodata);
                }
            }
            if verbose {
                progress = (100.0_f64 * row as f64 / (rows - 1) as f64) as i32;
                if progress != old_progress {
                    println!("Progress: {}%", progress);
                    old_progress = progress;
                }
            }
        }

        let mut num_classes = 0;
        for a in 0..256usize {
            if active_class[a] {
                num_classes += 1;
            }
        }

        let mut agreements = 0usize;
        let mut expected_frequency = 0f64;
        let mut n = 0usize;
        let mut row_total: usize;
        let mut col_total: usize;
        let kappa: f64;
        let overall_accuracy: f64;

        for a in 0..256usize {
            agreements += error_matrix[a][a];
            for b in 0..256usize {
                n += error_matrix[a][b];
            }
        }

        for a in 0..256usize {
            row_total = 0;
            col_total = 0;
            for b in 0..256usize {
                col_total += error_matrix[a][b];
                row_total += error_matrix[b][a];
            }
            expected_frequency += (col_total as f64 * row_total as f64) / (n as f64);
        }

        kappa = (agreements as f64 - expected_frequency as f64)
            / (n as f64 - expected_frequency as f64);
        overall_accuracy = agreements as f64 / n as f64;

        let mut f = File::create(output_file.as_str()).unwrap();

        let mut s = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">
        <head>
            <meta content=\"text/html; charset=UTF-8\" http-equiv=\"content-type\">
            <title>Lidar Kappa Index of Agreement</title>
            <style  type=\"text/css\">
                h1 {
                    font-size: 14pt;
                    margin-left: 15px;
                    margin-right: 15px;
                    text-align: center;
                    font-family: Helvetica, Verdana, Geneva, Arial, sans-serif;
                }
                h3 {
                    font-size: 12pt;
                    margin-left: 15px;
                    margin-right: 15px;
                    text-align: left;
                    font-family: Helvetica, Verdana, Geneva, Arial, sans-serif;
                }
                p, ol, ul, li {
                    font-size: 12pt;
                    font-family: Helvetica, Verdana, Geneva, Arial, sans-serif;
                    margin-left: 15px;
                    margin-right: 15px;
                }
                caption {
                    font-family: Helvetica, Verdana, Geneva, Arial, sans-serif;
                    font-size: 12pt;
                    margin-left: 15px;
                    margin-right: 15px;
                }
                table {
                    font-size: 12pt;
                    font-family: Helvetica, Verdana, Geneva, Arial, sans-serif;
                    font-family: arial, sans-serif;
                    border-collapse: collapse;
                    align: center;
                }
                td {
                    text-align: left;
                    padding: 8px;
                }
                th {
                    text-align: left;
                    padding: 8px;
                    background-color: #ffffff;
                    border-bottom: 1px solid #333333;
                    text-align: center;
                }
                tr:nth-child(1) {
                    border-bottom: 1px solid #333333;
                    border-top: 2px solid #333333;
                }
                tr:last-child {
                    border-bottom: 2px solid #333333;
                }
                tr:nth-child(even) {
                    background-color: #dddddd;
                }
                .numberCell {
                    text-align: right;
                }
                .headerCell {
                    text-align: center;
                }
            </style>
        </head>
        <body>";
        f.write(s.as_bytes()).unwrap();
        s = "<body><h1>Kappa Index of Agreement</h1>";
        f.write(s.as_bytes()).unwrap();
        let path = Path::new(&input_file1);
        let s1 = &format!(
            "<p><strong>Classification Data:</strong> {}</p>",
            path.file_name().unwrap().to_str().unwrap()
        );
        f.write_all(s1.as_bytes())?;
        let path = Path::new(&input_file2);
        let s1 = &format!(
            "<p><strong>Reference Data:</strong> {}</p><br>",
            path.file_name().unwrap().to_str().unwrap()
        );
        f.write_all(s1.as_bytes())?;
        // let s2 = &format!("{}{}{}{}{}", "<p><b>Input Data:</b> <br><br><b>Classification Data:</b> ", input_file1, "<br><br><b>Reference Data:</b> ", input_file2, "<p>");
        // f.write(s2.as_bytes()).unwrap();

        s = "<br><table>";
        f.write(s.as_bytes()).unwrap();
        s = "<caption>Contingency Table</caption>";
        f.write(s.as_bytes()).unwrap();
        s = "<tr>";
        f.write(s.as_bytes()).unwrap();
        let s3 = &format!(
            "{}{}{}",
            "<th colspan=\"2\" rowspan=\"2\"></th><th colspan=\"",
            num_classes,
            "\">Reference Data</th><th rowspan=\"2\">Row<br>Totals</th>"
        );
        f.write(s3.as_bytes()).unwrap();
        s = "</tr>";
        f.write(s.as_bytes()).unwrap();
        s = "<tr>";
        f.write(s.as_bytes()).unwrap();
        for a in 0..256 {
            if active_class[a] {
                let s = &format!(
                    "{}{}{}",
                    "<th>",
                    convert_class_val_to_class_string(a as u8),
                    "</th>"
                );
                f.write(s.as_bytes()).unwrap();
            }
        }

        s = "</tr>";
        f.write(s.as_bytes()).unwrap();
        let mut first_entry = true;
        for a in 0..256 {
            if active_class[a] {
                if first_entry {
                    let s = format!(
                        "{}{}{}{}{}",
                        "<tr><td rowspan=\"",
                        num_classes,
                        "\" valign=\"center\"><b>Class<br>Data</b></td> <td><b>",
                        convert_class_val_to_class_string(a as u8),
                        "</b></td>"
                    );
                    f.write(s.as_bytes()).unwrap();
                } else {
                    let s = format!(
                        "{}{}{}",
                        "<tr><td><b>",
                        convert_class_val_to_class_string(a as u8),
                        "</b></td>"
                    );
                    f.write(s.as_bytes()).unwrap();
                }
                row_total = 0;
                for b in 0..256 {
                    if active_class[b] {
                        row_total += error_matrix[a][b];
                        let s = format!(
                            "{}{}{}",
                            "<td class=\"numberCell\">", error_matrix[a][b], "</td>"
                        );
                        f.write(s.as_bytes()).unwrap();
                    }
                }
                let s = format!("{}{}{}", "<td class=\"numberCell\">", row_total, "</td>");
                f.write(s.as_bytes()).unwrap();

                let s2 = "</tr>";
                f.write(s2.as_bytes()).unwrap();
                first_entry = false;
            }
        }
        s = "<tr>";
        f.write(s.as_bytes()).unwrap();
        s = "<th colspan=\"2\">Column Totals</th>";
        f.write(s.as_bytes()).unwrap();
        for a in 0..256 {
            if active_class[a] {
                col_total = 0;
                for b in 0..256 {
                    if active_class[b] {
                        col_total += error_matrix[b][a];
                    }
                }
                let s = &format!("{}{}{}", "<td  class=\"numberCell\">", col_total, "</td>");
                f.write(s.as_bytes()).unwrap();
            }
        }

        let s4 = &format!(
            "{}{}{}",
            "<td class=\"numberCell\"><b>N</b>=", n, "</td></tr>"
        );
        f.write(s4.as_bytes()).unwrap();
        s = "</table>";
        f.write(s.as_bytes()).unwrap();
        s = "<br><br><table>";
        f.write(s.as_bytes()).unwrap();
        s = "<caption>Class Statistics</caption>";
        f.write(s.as_bytes()).unwrap();
        s = "<tr><th class=\"headerCell\">Class</th><th class=\"headerCell\">User's Accuracy<sup>1</sup><br>(Reliability)</th><th class=\"headerCell\">Producer's Accuracy<sup>1</sup><br>(Accuracy)</th></tr>";
        f.write(s.as_bytes()).unwrap();

        let mut average_producers = 0.0;
        let mut average_users = 0.0;
        let mut num_active = 0.0;
        for a in 0..256 {
            if active_class[a] {
                num_active += 1.0;
                let mut row_total = 0;
                let mut col_total = 0;
                for b in 0..256 {
                    if active_class[b] {
                        col_total += error_matrix[a][b];
                        row_total += error_matrix[b][a];
                    }
                }
                average_users += 100.0 * error_matrix[a][a] as f64 / col_total as f64;
                average_producers += 100.0 * error_matrix[a][a] as f64 / row_total as f64;
                let s = &format!(
                    "{}{}{}{}{}{}{}",
                    "<tr><td>",
                    convert_class_val_to_class_string(a as u8),
                    "</td><td class=\"numberCell\">",
                    format!(
                        "{:.*}",
                        2,
                        (100.0 * error_matrix[a][a] as f64 / col_total as f64)
                    ),
                    "%</td><td class=\"numberCell\">",
                    format!(
                        "{:.*}",
                        2,
                        (100.0 * error_matrix[a][a] as f64 / row_total as f64)
                    ),
                    "%</td></tr>"
                );
                f.write(s.as_bytes()).unwrap();
            }
        }
        f.write(format!("<tr><td>Average</td><td class=\"numberCell\">{}%</td><td class=\"numberCell\">{}%</td></tr>", format!("{:.*}", 2, average_users / num_active),
                format!("{:.*}", 2, average_producers / num_active)).as_bytes()).unwrap();

        s = "</table>";
        f.write(s.as_bytes()).unwrap();
        let s6 = &format!(
            "<p>{}{}</p>",
            "<p><b>Overall Accuracy</b> = ",
            format!("{:.*}%", 2, overall_accuracy * 100.0)
        );
        f.write(s6.as_bytes()).unwrap();
        let s7 = &format!(
            "<p><b>Kappa</b><sup>2</sup> = {}</p>",
            format!("{:.*}", 3, kappa)
        );
        f.write(s7.as_bytes()).unwrap();
        let s5 = &format!("{}{}", "<p><br>Notes:<br>1. User's accuracy refers to the proportion of points correctly assigned to a class (i.e. the number of points correctly classified for a category divided by the row total in the contingency table) and is a measure of the reliability. ",
                "Producer's accuracy is a measure of the proportion of the points in each category correctly classified (i.e. the number of points correctly classified for a category divided by the column total in the contingency table) and is a measure of the accuracy.<br>");
        f.write(s5.as_bytes()).unwrap();
        f.write("<br>2. Cohen's kappa coefficient is a statistic that measures inter-rater agreement for qualitative (categorical)
        items. It is generally thought to be a more robust measure than simple percent agreement calculation, since
        kappa takes into account the agreement occurring by chance. Kappa measures the percentage of data values in the
        main diagonal of the contingency table and then adjusts these values for the amount of agreement that could be expected due
        to chance alone.</p>".as_bytes()).unwrap();
        s = "</body>";
        f.write_all(s.as_bytes())?;

        let _ = f.flush();

        if verbose {
            if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
                let output = Command::new("open")
                    .arg(output_file.clone())
                    .output()
                    .expect("failed to execute process");

                let _ = output.stdout;
            } else if cfg!(target_os = "windows") {
                // let output = Command::new("cmd /c start")
                let output = Command::new("explorer.exe")
                    .arg(output_file.clone())
                    .output()
                    .expect("failed to execute process");

                let _ = output.stdout;
            } else if cfg!(target_os = "linux") {
                let output = Command::new("xdg-open")
                    .arg(output_file.clone())
                    .output()
                    .expect("failed to execute process");

                let _ = output.stdout;
            }

            println!("Complete!\nPlease see {} for output.", output_file);
        }

        let elapsed_time = get_formatted_elapsed_time(start);
        class_accuracy.add_metadata_entry(format!(
            "Created by whitebox_tools\' {} tool",
            self.get_tool_name()
        ));
        class_accuracy.add_metadata_entry(format!("Input file 1: {}", input_file1));
        class_accuracy.add_metadata_entry(format!("Input file 2: {}", input_file2));
        class_accuracy
            .add_metadata_entry(format!("Elapsed Time (excluding I/O): {}", elapsed_time));

        if verbose {
            println!("Saving data...")
        };
        let _ = match class_accuracy.write() {
            Ok(_) => {
                if verbose {
                    println!("Output file written")
                }
            }
            Err(e) => return Err(e),
        };

        if verbose {
            println!(
                "\n{}",
                &format!("Elapsed Time (excluding I/O): {}", elapsed_time)
            );
        }

        Ok(())
    }
}
