package com.sb.elsinore;

import com.sb.common.SBStringUtils;
import org.apache.commons.io.input.ReversedLinesFileReader;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import java.io.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 *
 *
 */
public class StatusRecorder implements Runnable {

    public static final String RECORDER = "recorder";
    public static final String RECORDER_TIME = "recorderTime";
    public static final String RECORDER_DIFF = "recorderDiff";

    public static double THRESHOLD = .15d;
    public static long SLEEP = 1000 * 5; // 5 seconds - is this too fast?
    private JSONObject lastStatus = null;
    private String logFile = null;
    private Thread thread;
    private String recorderDirectory = StatusRecorder.defaultDirectory;
    private HashMap<String, Status> temperatureMap;
    private HashMap<String, Status> dutyMap;
    boolean writeRawLog = false;
    public static String defaultDirectory = "graph-data/";
    public static String DIRECTORY_PROPERTY = "recorder_directory";
    public static String RECORDER_ENABLED = "recorder_enabled";
    private String currentDirectory = null;

    public StatusRecorder(String recorderDirectory) {
        this.recorderDirectory = recorderDirectory;
    }

    /**
     * Start the thread.
     */
    public final void start() {
        if (thread == null || !thread.isAlive()) {
            temperatureMap = new HashMap<>();
            dutyMap = new HashMap<>();
            thread = new Thread(this);
            thread.setName("Status recorder");
            thread.start();
        }
    }

    /**
     * Stop the thread.
     */
    public final void stop() {
        if (thread != null) {
            thread.interrupt();
            while (thread.isAlive()) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            thread = null;
        }
    }

    /**
     * Save a specific value to the status recorder data files.
     * @param name The name to save to.
     * @param value The value to store
     */
    public void saveReading(String name, BigDecimal value)
    {
        File tempFile = new File(currentDirectory + name + "-manual.csv");
        appendToLog(tempFile, new Date().getTime() + "," + value.toPlainString() + "\r\n");
    }

    /**
     * Main runnable, updates the files every five seconds.
     */
    @Override
    public final void run() {
        //This will store multiple logs - one for raw data,
        // one for each series (duty & temperature per vessel)
        // For now - we'll store Duty, temperature vs time
        //Assume new logs on each run
        
        //Keep checking the status until all the temperature sensors are initialized
        try {
            
            while (!checkInitialized()) {
                Thread.sleep(1000);
            }

            long startTime = System.currentTimeMillis();

            currentDirectory = recorderDirectory + "/" + startTime + "/";
            File directoryFile = new File(currentDirectory);
            if (!directoryFile.mkdirs()) {
                BrewServer.LOG.warning("Could not create directory: " + currentDirectory);
                return;
            }
            LaunchControl.setFileOwner(directoryFile.getParentFile());
            LaunchControl.setFileOwner(directoryFile);

            //Generate a new log file under the current directory
            logFile = currentDirectory + "raw.log";

            File file = new File(this.logFile);
            boolean fileExists = file.exists();
            LaunchControl.setFileOwner(file);

            boolean continueRunning = true;
            while (continueRunning) {
                //Just going to record when something changes
                try {
                    String status = LaunchControl.getJSONStatus();
                    JSONObject newStatus = (JSONObject) JSONValue.parse(status);
                    if (lastStatus == null || isDifferent(lastStatus, newStatus)) {
                        //For now just log the whole status
                        //Eventually we may want multiple logs, etc.
                        if (writeRawLog) {
                            writeToLog(newStatus, fileExists);
                        }

                        Date now = new Date();
                        printJsonToCsv(now, newStatus, currentDirectory);
                        lastStatus = newStatus;
                        fileExists = true;
                    }
                } catch (Exception ioe) {
                    continueRunning = false;
                }
                Thread.sleep(SLEEP);
            }
        } catch (InterruptedException ex) {
            BrewServer.LOG.warning("Status Recorder shutting down");
        }

    }
    
    protected boolean checkInitialized()
    {
        return LaunchControl.isInitialized();
    }

    /**
     * Save the status to the directory.
     *
     * @param nowDate The current date to save the datapoint for.
     * @param newStatus The JSON Status object to dump
     * @param directory The graph data directory.
     */
    protected final void printJsonToCsv(final Date nowDate,
            final JSONObject newStatus, final String directory) {
        //Now look for differences in the temperature and duty
        long now = nowDate.getTime();
        JSONArray vessels = (JSONArray) newStatus.get("vessels");
        for (Object vessel1 : vessels) {
            JSONObject vessel = (JSONObject) vessel1;
            if (vessel.containsKey("name")) {
                String name = vessel.get("name").toString();
                Temp currentTemp = LaunchControl.findTemp(name);
                if (currentTemp != null) {
                    name = currentTemp.getProbe();
                }
                if (vessel.containsKey("tempprobe")) {
                    String temp = ((JSONObject) vessel.get("tempprobe"))
                            .get("temp").toString();


                    Status lastStatus = temperatureMap.get(name);
                    if (lastStatus == null) {
                        lastStatus = new Status("-999", now);
                    }

                    if (lastStatus.isDifferentEnough(temp)) {
                        File tempFile = new File(directory + name + "-temp.csv");
                        if (now - lastStatus.timestamp > SLEEP * 1.5) {
                            appendToLog(tempFile, now - SLEEP + "," + lastStatus.value + "\r\n");
                        }
                        appendToLog(tempFile, now + "," + temp + "\r\n");

                        temperatureMap.put(name, new Status(temp, now));
                    }
                }

                if (vessel.containsKey("pidstatus")) {
                    JSONObject pid = (JSONObject) vessel.get("pidstatus");
                    String duty = "0";
                    if (pid.containsKey("actualduty")) {
                        duty = pid.get("actualduty").toString();
                    } else if (!pid.get("mode").equals("off")) {
                        duty = pid.get("duty").toString();
                    }

                    Status lastStatus = dutyMap.get(name);
                    if (lastStatus == null) {
                        lastStatus = new Status("-999", now);
                    }

                    if (!duty.equals(lastStatus.value)) {
                        File dutyFile = new File(directory + name + "-duty.csv");
                        if (now - lastStatus.timestamp > SLEEP * 1.5) {
                            appendToLog(dutyFile, now - SLEEP + "," + lastStatus.value + "\r\n");
                        }
                        appendToLog(dutyFile, now + "," + duty + "\r\n");
                        dutyMap.put(name, new Status(duty, now));
                    }
                }

            }
        }
    }

    /**
     * Save the string to the log file.
     *
     * @param file The file object to save to
     * @param toAppend The string to add to the file
     */
    protected final void appendToLog(final File file, final String toAppend) {
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter(file, true);
            fileWriter.write(toAppend);
        } catch (IOException ex) {
            BrewServer.LOG.warning("Could not save to file: "
                    + file.getAbsolutePath());
        } finally {
            try {
                if (fileWriter != null) {
                    fileWriter.close();
                }
            } catch (IOException ex) {
                BrewServer.LOG.warning("Could not close filewriter: "
                        + file.getAbsolutePath());
            }
        }
    }

    /**
     * Write a JSON object to the log file.
     *
     * @param status The JSON Object to log
     * @param fileExists If the file exists, prepend a "," otherwise an open
     * brace "["
     */
    protected final void writeToLog(final JSONObject status,
            final boolean fileExists) {
        String append = fileExists ? "," : "[" + status.toJSONString();
        appendToLog(new File(this.logFile), append);
    }

    /**
     * Check to see if the objects are different.
     *
     * @param previous The first object to check.
     * @param current The second object to check
     * @return True if the objects are different
     */
    protected final boolean isDifferent(final JSONObject previous,
            final JSONObject current) {
        if (previous.size() != current.size()) {
            return true;
        }

        for (Object key : previous.keySet()) {
            if (!"elapsed".equals(key)) {
                Object previousValue = previous.get(key);
                Object currentValue = current.get(key);

                if (compare(previousValue, currentValue)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Check to see if the JSONArrays are different.
     *
     * @param previous The first JSONArray to check
     * @param current The second JSONArray to check.
     * @return True if the JSONArrays are different
     */
    protected final boolean isDifferent(final JSONArray previous,
            final JSONArray current) {

        if (previous.size() != current.size()) {
            return true;
        }

        for (int x = 0; x < previous.size(); x++) {
            Object previousValue = previous.get(x);
            Object currentValue = current.get(x);

            if (compare(previousValue, currentValue)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Compare two generic objects.
     *
     * @param previousValue First object to check
     * @param currentValue Second object to check
     * @return True if the objects are different, false if the same.
     */
    protected final boolean compare(final Object previousValue,
            final Object currentValue) {
        if (previousValue == null && currentValue == null) {
            return true;
        }

        if (previousValue == null || currentValue == null) {
            return false;
        }

        if (previousValue instanceof JSONObject
                && currentValue instanceof JSONObject) {
            if (isDifferent((JSONObject) previousValue,
                    (JSONObject) currentValue)) {
                return true;
            }
        } else if (previousValue instanceof JSONArray
                && currentValue instanceof JSONArray) {
            if (isDifferent((JSONArray) previousValue,
                    (JSONArray) currentValue)) {
                return true;
            }
        } else {
            if (!previousValue.equals(currentValue)) {
                return true;
            }
        }

        return false;
    }

    private class Status {

        public long timestamp;
        public String value;

        public Status(String value, long timestamp) {
            this.value = value;
            this.timestamp = timestamp;
        }

        public boolean isDifferentEnough(String newValue) {
            boolean retVal = false;

            try {
                double oldVal = Double.valueOf(value);
                double newVal = Double.valueOf(newValue);
                retVal = Math.abs(oldVal - newVal) > THRESHOLD;
            } catch (Throwable ignored) {
            }

            return retVal;

        }
    }

    /**
     * Set the Threshold for the status recorder.
     * @param recorderDiff The threshold to use.
     */
    public void setThreshold(double recorderDiff) {
        THRESHOLD = recorderDiff;
    }
    
    public double getDiff() {
        return StatusRecorder.THRESHOLD;
    }
    
    public double getTime() {
        return StatusRecorder.SLEEP;
    }

    public void setDiff(double threshold) {
        StatusRecorder.THRESHOLD = threshold;
    }
    
    public void setTime(long time) {
        StatusRecorder.SLEEP = time;
    }

    public String getCurrentDir() {
        return this.currentDirectory;
    }

    @SuppressWarnings("unchecked")
    public NanoHTTPD.Response getData(Map<String, String> params) {
        String rootPath;
        try {
            rootPath = SBStringUtils.getAppPath("");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return new NanoHTTPD.Response("No app path");
        }

        int size = 1000;
        if (params.containsKey("size")) {
            try {
                size = Integer.parseInt(params.get("size"));
            } catch (NumberFormatException nfe) {
                size = 1000;
            }
        }

        String vessel = "";
        if (params.containsKey("vessel")) {
            vessel = params.get("vessel");
        }
        String vesselName = vessel;
        Temp temp = LaunchControl.findTemp(vessel);
        if (temp != null) {
            vesselName = temp.getProbe();
        }

        File[] contents = new File(getCurrentDir()).listFiles();
        JSONObject xsData = new JSONObject();
        JSONObject axes = new JSONObject();
        JSONArray dataBuffer = new JSONArray();
        long currentTime = System.currentTimeMillis();

        // Are we downloading the files?
        if (params.containsKey("download")
                && params.get("download").equalsIgnoreCase("true")) {

            String zipFileName = rootPath + "/graph-data/zipdownload-" + currentTime + ".zip";
            ZipFile zipFile = null;
            try {
                zipFile = new ZipFile(zipFileName);
            } catch (FileNotFoundException ioe) {
                BrewServer.LOG.warning(
                        "Couldn't create zip file at: " + zipFileName);
                BrewServer.LOG.warning(ioe.getLocalizedMessage());
            }

            if (contents == null || zipFile == null) {
                return new NanoHTTPD.Response(NanoHTTPD.Response.Status.BAD_REQUEST, BrewServer.MIME_TYPES.get("json"),
                        "No files.");
            }

            for (File content : contents) {
                try {

                    if (content.getName().endsWith(".csv")
                            && content.getName().toLowerCase()
                            .startsWith(vesselName.toLowerCase())) {
                        zipFile.addToZipFile(content.getAbsolutePath());
                    }
                } catch (IOException ioe) {
                    BrewServer.LOG.warning(
                            "Couldn't add " + content.getAbsolutePath()
                                    + " to zipfile");
                }
            }
            try {
                zipFile.closeZip();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return BrewServer.serveFile("graph-data/zipdownload-" + currentTime + ".zip",
                    params, new File(rootPath));
        }

        if (contents == null) {
            return new NanoHTTPD.Response(NanoHTTPD.Response.Status.BAD_REQUEST, BrewServer.MIME_TYPES.get("json"),
                    "{Bad: Request}");
        }
        boolean dutyVisible = false;
        for (File content : contents) {
            if (content.getName().endsWith(".csv")
                    && content.getName().toLowerCase()
                    .startsWith(vesselName.toLowerCase())) {
                String name = content.getName();
                String localName;

                // Strip off .csv
                name = name.substring(0, name.length() - 4);
                localName = name.substring(0, name.lastIndexOf("-"));
                String type = name.substring(name.lastIndexOf("-") + 1);
                Temp localTemp = LaunchControl.findTemp(localName);
                if (localTemp != null) {
                    localName = localTemp.getName();
                } else {
                    localName = name;
                }
                localName = org.apache.commons.lang3.StringEscapeUtils.unescapeJson(localName);
                if (params.containsKey("bindto")
                        && (params.get("bindto"))
                        .endsWith("-graph_body")) {
                    localName = type;
                }

                // Work out the real axis name and reuse it.
                String axisName = type.toUpperCase();
                if (!localName.equals(type)) {
                    axisName = localName + " " + type;
                }

                xsData.put(axisName, "x" + axisName);
                if (type.equalsIgnoreCase("duty")) {

                    axes.put(axisName, "y");
                    dutyVisible = true;
                } else {
                    axes.put(axisName, "y2");
                }

                JSONArray xArray = new JSONArray();
                JSONArray dataArray = new JSONArray();

                xArray.add("x" + axisName);
                dataArray.add(axisName);

                ReversedLinesFileReader reader = null;
                try {
                    reader = new ReversedLinesFileReader(content);
                    String line;
                    String[] lArray ;

                    int count = 0;
                    try {
                        while ((line = reader.readLine()) != null && count < size) {
                            // Each line contains the timestamp and the value
                            lArray = line.split(",");
                            BrewServer.LOG.info("Line: " + line + ". Split: " + lArray.length);
                            if (lArray.length != 2) {
                                continue;
                            }
                            long timestamp = Long.parseLong(lArray[0]);
                            // If this is the first element, add an extra one on.
                            if (count == 0 && timestamp != currentTime) {
                                xArray.add(BrewDay.mFormat
                                        .format(new Date(currentTime)));
                                dataArray.add(lArray[1].trim());
                            }
                            xArray.add(BrewDay.mFormat
                                            .format(new Date(timestamp))
                            );
                            dataArray.add(lArray[1].trim());
                            count++;
                        }
                    } catch (Exception e) {
                        // Do nothing. File doesn't have any data.
                        BrewServer.LOG.info("Error when reading for temperature: " + content.getAbsolutePath());
                    }



                    dataBuffer.add(xArray);
                    dataBuffer.add(dataArray);
                    BrewServer.LOG.info("Read: " + count);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (Exception e) {
                            BrewServer.LOG.warning("Couldn't close file: "
                                    + content.getAbsolutePath());
                        }
                    }
                }

            }
        }

        JSONObject dataContent = new JSONObject();
        dataContent.put("columns", dataBuffer);
        if (params.containsKey("updates")
                && Boolean.parseBoolean(params.get("updates"))) {
            return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, BrewServer.MIME_TYPES.get("json"),
                    dataContent.toJSONString());
        }

        dataContent.put("xs", xsData);
        dataContent.put("axes", axes);
        dataContent.put("xFormat", "%Y/%m/%d %H:%M:%S%Z");

        // DO the colours manually
        JSONObject colorContent = new JSONObject();
        if (!vessel.equals("")) {
            JSONArray tJson;
            for (Object aDataBuffer : dataBuffer) {
                tJson = (JSONArray) aDataBuffer;
                String series = (String) tJson.get(0);
                String color = "";
                if (series.equals("temp")) {
                    color = "#00ff00";
                } else if (series.equals("duty")) {
                    color = "#0000ff";
                }
                colorContent.put(series, color);
            }
            dataContent.put("colors", colorContent);
        }

        JSONObject axisContent = new JSONObject();


        JSONObject y1Label = new JSONObject();
        y1Label.put("text", "Temperature");
        y1Label.put("position", "outer-middle");
        JSONObject y1 = new JSONObject();
        y1.put("show",  "true");
        y1.put("label", y1Label);

        JSONObject padding = new JSONObject();
        padding.put("top", 0);
        padding.put("bottom", 0);
        y1.put("padding", padding);

        JSONObject formatJSON = new JSONObject();
        formatJSON.put("format", "%H:%M:%S");
        formatJSON.put("culling", "{max: 4}");
        formatJSON.put("rotate", 90);
        JSONObject xContent = new JSONObject();
        xContent.put("type", "timeseries");
        xContent.put("tick", formatJSON);
        axisContent.put("x", xContent);
        axisContent.put("y2", y1);
        if (dutyVisible) {
            JSONObject y2Label = new JSONObject();
            y2Label.put("text", "Duty Cycle %");
            y2Label.put("position", "outer-middle");
            JSONObject y2 = new JSONObject();
            y2.put("show", "true");
            y2.put("label", y2Label);
            axisContent.put("y", y2);
        }

        JSONObject finalJSON = new JSONObject();
        finalJSON.put("data", dataContent);
        finalJSON.put("axis", axisContent);

        if (params.containsKey("bindto")) {
            finalJSON.put("bindto", "[id='" + params.get("bindto") + "']");
        } else {
            finalJSON.put("bindto", "#chart");
        }

        if (!((String) finalJSON.get("bindto")).endsWith("_body")) {
            JSONObject enabledJSON = new JSONObject();
            enabledJSON.put("enabled", true);
            finalJSON.put("zoom", enabledJSON);
        }

        return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, BrewServer.MIME_TYPES.get("json"),
                finalJSON.toJSONString());
    }

    public NanoHTTPD.Response deleteAllData() {
        File graphDir = new File(this.recorderDirectory);
        File[] fileList = graphDir.listFiles();

        if (fileList != null) {
            for (File directory : fileList) {
                if (!deleteDir(directory)) {
                    BrewServer.LOG.warning("Failed to delete: " + directory.getAbsolutePath());
                }
            }
        }
        return new NanoHTTPD.Response(NanoHTTPD.Response.Status.OK, BrewServer.MIME_TYPES.get("json"),
                "{Complete}");
    }

    public boolean deleteDir(File file) {
        if (file.isDirectory()) {
            String[] children = file.list();
            for (String childDir : children) {
                boolean success = deleteDir(new File(file, childDir));
                if (!success) {
                    return false;
                }
            }
        }
        return file.delete();
    }

}