package usda.weru.mcrew;

/*--- IMPORTS ------------------------------------------------------------------*/
import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.google.gson.Gson; 
import com.google.gson.GsonBuilder;
import de.schlichtherle.truezip.fs.FsSyncException;
import java.io.BufferedReader;
import java.io.Reader;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime; 
import javax.swing.JOptionPane;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
/*--- END IMPORTS --------------------------------------------------------------*/

/**
 * May 6th, 2020
 * @author cademccumber
 * 
 * The purpose of the class is to convert WEPP management CRLMOD files into WEPS
 * formatted XML files (In this project appended with '.skel' for 'skeleton' file.
 */
public class JSONUpdater {

    /**
     * DEBUG flag. If true, some informative print statements are turned on for this class.
     * Used for output that when not working on class would omitted. Debug color displayed 
     * in purple to distinguish between regular output and debug. TURN OFF when not in use. */
    private final boolean _DEBUG = false;
    // Used to change console output color for _DEBUG flag
    public static final String PURPLE = "\033[0;35m";
    public static final String RESET = "\033[0m";  // Resets to default color
    
    private File temporarySkelFile;
    
    private MCREWWindow source;
    
    /**
     * No args CTOR
     */
    protected JSONUpdater(MCREWWindow source) { 
        this.source = source;
    }
    
    /**
     * 
     * @param m_filename the full path of the file to open and translate to XML
     * @param new_skel_file the name of the new (or existing) file to open
     * @returns True upon success, false upon failure
     */
    protected boolean write_to_XML(String selected_filename, String new_skel_file) {
        
        if (_DEBUG) { // Information for debug
            System.out.println(PURPLE + "--------------------------------------------------------" + RESET);
            System.out.println(PURPLE + "=== DEBUG ===" + RESET + " is ON for JSONUpdater. Appearing in " + PURPLE + "PURPLE" + RESET);
            System.out.println(PURPLE + "--------------------------------------------------------" + RESET);
            System.out.println(PURPLE + "[ JSON ---> SKEL" + RESET);
        }
        
        File json_file = open_json_file(selected_filename);
        File new_file = open_or_create_file(new_skel_file);                         // create file to write to
        if (new_file == null || json_file == null) {
            return false;                                                           // can't write/read files, return false
        }
        return buildXML(new_file, json_file) != null;                               // return success of building xml file
    }
    
    /**
     * This returns a temporary file to be deleted on exit,
     * that is a temporary .skel file to be processed and translated
     * @param selected_filename the filename of the file to be translated 
     * @returns null with failure, tmp .skel file on success
     */
    protected File write_to_MAN(String selected_filename) {
        if (_DEBUG) { // Information for debug
            System.out.println(PURPLE + "--------------------------------------------------------" + RESET);
            System.out.println(PURPLE + "=== DEBUG ===" + RESET + " is ON for JSONUpdater. Appearing in " + PURPLE + "PURPLE" + RESET);
            System.out.println(PURPLE + "--------------------------------------------------------" + RESET);
            System.out.println(PURPLE + "[ JSON ---> MAN" + RESET);
        }
        
        File json_file = open_json_file(selected_filename);
        File tempFile;
        // Create temp file to write to skel. Temp file can then be returned to translate to permanent man file
        try {
            tempFile = File.createTempFile("tempSkelFile", ".skel");
            tempFile.deleteOnExit(); // set temp file to be deleted when java virtual machine exits (program terminates)
        } catch (IOException io) {
            // Temp file not created
            System.err.println("Temp file for conversion failed to create in \'write_to_MAN\'");
            return null;
        }

        return buildXML(tempFile, json_file); // Return created file
    }
    
    /**
     * this is the driver method for actually writing the json file to a skel file
     * ADOPTED FROM: https://www.journaldev.com/1112/how-to-write-xml-file-in-java-dom-parser
     */
    private File buildXML(File new_file, File json_file)  {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder;
        
        try {
            // Create document
            builder = factory.newDocumentBuilder();
            Document document = XMLDoc.createDocument(XMLConstants.smanagement_skeleton);
            // Set up header for skel XML files
            Element root = document.getDocumentElement();
            root.setAttribute("ver", "1.0"); // TODO this is hardcoded, should it be? It is elsewhere
            root.setAttribute("prog", usda.weru.util.Application.WEPS.getName());
            // Get rotation data from json file into RotationData object
            RotationData rotationData = toGSON(json_file);    
            
            // Prints contents returned from GSON for dev/debugging purposes
            if (_DEBUG) test_GSON_output(rotationData); 
                        
            // using rotation data, beuild the root element into skel data
            write_skel_file(root, rotationData, document);
                  
            // write to file
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            // This sets indent amount to 4 spaces
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            // Needed to write to file
            DOMSource source = new DOMSource(document);
            // data stream that writes to file
            StreamResult xml_file = new StreamResult(new_file);
            // transformer applies indents and formatting
            transformer.transform(source, xml_file);
            
        } catch (NullPointerException nullP) {
            // Most Likely this is a .json file that is not management format.
            if (json_file != null)
                System.err.println("File: " + json_file.getAbsolutePath() + " could not be converted.\n" +
                    "\tPlease check that file follows WEPP json format.");
            //nullP.printStackTrace();
            return null;
        } catch (Exception e) {
            System.err.println("Exception in write_to_XML\n"); 
            e.printStackTrace(); 
            return null;
        }
        return new_file;
    }
    
    /**
     * This function writes the actual contents of the operations, using the rotationData
     * passed to it. 
     * @param root the root element of the document
     * @param rotationData a RotationData object containing all the information read from json
     * @param document 
     */
    private void write_skel_file(Element root, RotationData rotationData, Document document) {
        
        // Append notes element (Contains date of creation currently.
        Element notes = document.createElement("notes");
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        notes.setTextContent("SKEL translated from WEPP json file on " + formatter.format(LocalDateTime.now()));
        root.appendChild(notes);
        
        // Append name element
        Element name = document.createElement("name");
        name.setTextContent(rotationData.getName());
        root.appendChild(name);
        
        // Append rotationYears element (Duration in json file)
        Element duration = document.createElement("rotationyears");
        duration.setTextContent(rotationData.getDuration().toString()); 
        root.appendChild(duration);
        
        // open operation tag
        Element operations = document.createElement("operations");
        
        // get list of operations
        OperationData[] opData = rotationData.getOperationDataArray();
        
        // write op elements:
        for (int i = 0; i < opData.length; i++ ){
            operations.appendChild(write_operation_element(opData[i], document));
        }
        // add operations <op> tags
        root.appendChild(operations);
    }
    
    /**
     * Creates an <op> tag based on the OperationData object passed to it
     * @param opData - single OperationData element containing all information for an <op> tag
     * @param document
     * @return filled <op> element from opData
     */
    private Element write_operation_element(OperationData opData, Document document) {
        Element op = document.createElement("op");
        
        // create date tag
        Element date = document.createElement("date");
        date.setTextContent(opData.getDate());
        op.appendChild(date);
        
        // create name tag
        Element name = document.createElement("name");
        name.setTextContent(opData.getName());
        op.appendChild(name);
        
        // veg could have 0, 1, or 2 names
        if (opData.hasVeg_names()) {
            Element veg = document.createElement("veg");
            // name tag here is nested in <veg> tag
            for (int i = 0; i < opData.getVeg_names().length; i++) {
                // Check if name exists
                if (opData.getVeg_names()[i] == null)
                    continue;
                // Exists, add to element
                Element veg_name = document.createElement("name");
                veg_name.setTextContent(opData.getVeg_names()[i]);
                veg.appendChild(veg_name);
            }
            op.appendChild(veg);
        }   
        
        // If yield, add yield tag
        if (opData.getYield() != null) {
            Element yield = document.createElement("yield");
            yield.setTextContent(opData.getYield().toString());
            op.appendChild(yield);
        }
        
        // if res_added, add tag
        if (opData.getRes_added() != null) {
            Element res_added = document.createElement("res_added");
            res_added.setTextContent(convertWEPP_residue(opData.getRes_added()).toString());
            op.appendChild(res_added); 
        }
      
        return op;
    }
    
    final static Double WEPP_CONVERSION_UNIT = 8921.79;
    
    /**
     * This method converts the incoming WEPP value for a residue change from the 
     * WEPP dry-unit (lbs/acre) to the WEPS dry-unit SI (kg/m^2)
     * @param mass - the mass in (lbs/acre) to be converted
     * @return the converted value
     */
    private Double convertWEPP_residue(Integer mass) {
        //System.out.println("\n\tCONVERTED kg/m^2 VALUE: " + (mass/WEPP_CONVERSION_UNIT) + "\n");
        return (mass / WEPP_CONVERSION_UNIT); 
    }
    
    /**
     * This opens a file reader, and uses GSON and the reader to write data from json
     * file into a rotation object. Upon success, it returns rotation object
     * @param json_file
     * @return rotation object from json_file, null on failure
     */
    private RotationData toGSON(File json_file) {
        try {
            Reader reader = new BufferedReader(new TFileReader(new TFile(json_file)));  // Needs to be TFile for zip files

            Gson gson = new GsonBuilder().create();
            RotationData rotationData = gson.fromJson(reader, RotationData.class);      // read data into a Rotation object
            
            reader.close();
            return rotationData;
        } catch (FileNotFoundException notFound) {
            // This is already tested, if file did not exist prior to this, error is returned.
            System.err.println("FileNotFoundException in toGSON. File: " + json_file.getAbsolutePath());
        } catch (IOException io) { 
            System.err.println("Reader in \'toGSON\' failed to close");
        }
        return null;                                                                   // on failure, return nothing
    }
    
    /**
     * opens json file. All files should be vetted by calling program and should be json extensions, 
     * hence no check here.
     * @param selected_filename
     * @return file if open, null otherwise
     */
    private File open_json_file(String selected_filename) {
        File file;
        if (_DEBUG) System.out.println(PURPLE + "=== DEBUG === FILE NAME (opening): " + selected_filename + RESET);
        try {
            file = new File(selected_filename);
            if (_DEBUG) System.out.println(PURPLE + "=== DEBUG === json file opened " + RESET);
            
            // Validate file
            if(_DEBUG) System.out.println(PURPLE + "=== DEBUG === Validating json" + RESET);
            
            if (!isValidJSON(file)) {
                System.err.println("JSON file not a valid WEPP managment file: " + selected_filename);
                return null;
            }
            
            return file;
        } catch (Exception e) {
            System.err.println("Error in opening file: " + selected_filename);
            return null;
        }
    }
    
    /**
     * In lieu of a schema validation, this checks a few details of the json file
     * before using it. SHould at least prevent translation attempts of obviously
     * not WEPP management files. TODO - create schema and validate better
     * @param file
     * @return true if matching WEPP json form, false if not
     */
    private boolean isValidJSON(File file) {
        JSONParser parser = new JSONParser();
        TFileReader tfileReader;
        try {
            // Try to get json object from parser, if fails, not valid json
            tfileReader = new TFileReader(new TFile(file)); // Needs to be TFile for .zip files
            JSONObject json = (JSONObject) parser.parse(tfileReader);
            
            // get rotation object (main json object for file)
            if (!json.containsKey("rotation")) {
                // Check for 'other' version of WEPP files we don't support. Inform user
                if (json.containsKey("lmod_file")) {

                    JOptionPane.showMessageDialog(source, "The file: " + file.getAbsolutePath() +
                            "\nAppears to be a LMOD management file, which cannot be converted. \n" +
                            "Follow conversion instructions in WEPP to convert a WEPS compatible file.", "File Type Error", 
                            JOptionPane.ERROR_MESSAGE);

                    return false;
                }
                return false;
            }
            JSONObject rotation = (JSONObject) json.get("rotation");
            
            // check for duration
            if (!rotation.containsKey("duration"))
                return false;
            // check for list of managements
            if (!rotation.containsKey("managements"))
                return false;
            
            // close reader or causes 
            tfileReader.close();
            
            // basic requirements met
            if (_DEBUG) System.out.println(PURPLE + "=== DEBUG === basic requirements for json have been met, returning valid" + RESET);
            return true;
        } catch (FsSyncException fs) {
            System.err.println("FsSync Exception in isValidJSON() - likely caused by TFileReader not closing.");
        } catch (IOException io) {
            System.err.println(PURPLE + "IOException in validating json in JSONUpdater" + RESET);
            if (_DEBUG) io.printStackTrace();
        } catch (org.json.simple.parser.ParseException parse) {
            System.err.println("Failed to parse json file: " + file.getAbsolutePath());
            if (_DEBUG) parse.printStackTrace();
        }
        return false;
    }
    
    /**
     * returns file or creates a new one if did not exist
     * @param new_skel_file
     * @return 
     */
    private File open_or_create_file(String new_skel_file) {
        File file;
        if (_DEBUG) System.out.println(PURPLE + "=== DEBUG === FILE NAME (opening/already exists): " + new_skel_file + RESET);
        try {
            file = new File(new_skel_file);
            if (file.createNewFile()) {
                if (_DEBUG) System.out.println(PURPLE + "=== DEBUG === New file created for skel to be written to" + RESET);
            }
            if (_DEBUG) System.out.println(PURPLE + "=== DEBUG === Returning created or opened file for skel" + RESET);
            return file;
        } catch (NullPointerException n) {
            if (_DEBUG) System.err.println(PURPLE + "=== DEBUG === Null pointer Exception in \"open_or_create_file\""
                    + " opening the new skel file." + RESET);
            return null;
        } catch (IOException e) {
            if (_DEBUG) System.err.println(PURPLE + "=== DEBUG === IO Exception in \"open_or_create_file\"" + RESET);
            return null;
        }
    }
    
    /**
     * This function is purely for testing and has no application other than outputting 
     * the contents of the GSON object for debugging purposes
     * @param rotationData 
     */
    private void test_GSON_output(RotationData rotationData) {
        System.out.println(PURPLE + "************************************\n"+ RESET);
        System.out.println(PURPLE + "JSON to GSON DATA" + RESET);
        System.out.println(PURPLE + "DURATION: " + rotationData.getDuration() + RESET);
        System.out.println(PURPLE + "NAME: " + rotationData.getName() + RESET + "\n");
        OperationData[] rData = rotationData.getOperationDataArray();
        for (int i = 0; i < rData.length; i++) {
            System.out.println(PURPLE + "\top name:         " + rData[i].getName() + RESET);
            System.out.println(PURPLE + "\top date:         " + rData[i].getDate() + RESET);
            if (rData[i].hasVeg_names()) {
                System.out.print(PURPLE + "\top veg CROP:     " + RESET); 
                System.out.println(PURPLE + (rData[i].getVeg_names()[0] != null ? rData[i].getVeg_names()[0] : "(none)") + RESET);
                System.out.print(PURPLE + "\top veg RESIDUE:  " + RESET); 
                System.out.println(PURPLE + (rData[i].getVeg_names()[1] != null ? rData[i].getVeg_names()[1] : "(none)") + RESET);
            }
            System.out.print(PURPLE + "\t\tyield:           " + RESET);
            System.out.println(PURPLE + (rData[i].getYield() != null ? rData[i].getYield().intValue() : "(none)") + RESET);
            System.out.print(PURPLE + "\t\tres_added:       " + RESET);
            System.out.println(PURPLE + (rData[i].getRes_added() != null ? rData[i].getRes_added().intValue() : "(none)") + RESET);
                   
            System.out.println();
        }
        System.out.println(PURPLE + "************************************" + RESET);
    }
    
}
