package usda.weru.weps.reports;

import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileOutputStream;
import de.schlichtherle.truezip.file.swing.TFileChooser;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.sql.SQLException;
import java.util.Hashtable;
import java.util.Map;
import javax.swing.JFrame;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperPrint;
import org.apache.log4j.Logger;
import org.openide.util.Exceptions;
import usda.weru.soil.Soil;
import usda.weru.util.About;
import usda.weru.util.ConfigData;
import usda.weru.util.FileAgeChecksum;
import usda.weru.util.WepsFileChooser;
import usda.weru.weps.RunFileData;
import usda.weru.weps.reports.query.WepsConnection;

/**
 *
 * @author joelevin
 */
public class ReportManager {

    private static final Logger LOGGER = Logger.getLogger(ReportManager.class);
    private static ReportManager c_default;    //run dir -> reportname -> pack
    private Map<TFile, Map<String, Reference<ReportPack>>> c_packs;    //run dir -> reportname -> frame
    private Map<TFile, Map<String, JFrame>> c_openReports;

    /**
     *
     */
    public static final String REPORT_DETAIL = "detail";

    /**
     *
     */
    public static final String REPORT_SOIL = "soil";

    /**
     *
     */
    public static final String REPORT_DEBUG = "debug";

    /**
     *
     */
    public static final String REPORT_RUN = "run";

    /**
     *
     */
    public static final String REPORT_MANAGEMENT = "management";

    /**
     *
     */
    public static final String REPORT_CROPSUM = "cropsummary";

    /**
     * The string needed to generate the crop detail report.
     */
    public static final String REPORT_CROPDET = "cropdetail";

    /**
     * String needed to generate cover crop summary
     */
    public static final String REPORT_COVERCROPSUM = "covercropsummary";

    /**
     * The string needed to generate the cover crop detail report.
     */
    public static final String REPORT_COVERCROPDET = "covercropdetail";

    /**
     * String needed to generate crop interval summary
     */
    public static final String REPORT_CROP_INT_SUM = "cropintervalsum";

    /**
     * The string needed to generate the crop interval detail report.
     */
    public static final String REPORT_CROP_INT_DET = "cropintervaldetail";

    /**
     * The string needed to generate the crop interval detail report.
     */
    public static final String REPORT_CROP_INT_PER_DET = "cropintervalperioddetail";

    /**
     *
     */
    public static final String REPORT_STIR = "stir";

    /**
     *
     */
    public static final String REPORT_QUICKPLOT = "quickplot";

    /**
     *
     */
    public static final String REPORT_CONFIDENCE_INTERVAL = "ci";

    /**
     *
     */
    public static final String REPORT_DATABASE_CROPS = "databasecrops";

    /**
     *
     */
    public static final String REPORT_DATABASE_OPERATIONS = "databaseoperations";

    /**
     *
     */
    public static final String REPORT_DATABASE_MANAGEMENTS = "databasemanagements";

    //static {
    //assign default jr properties
    //JRProperties.setProperty(JRFont.DEFAULT_PDF_EMBEDDED, true);
    //}
    /**
     *
     */
    public ReportManager() {
    }

    /**
     *
     * @return
     */
    public synchronized static ReportManager getDefault() {
        if (c_default == null) {
            c_default = new ReportManager();
        }
        return c_default;
    }

    /**
     *
     * @param file
     * @param reportName
     * @return
     */
    public synchronized ReportPack getReportPack(TFile file, String reportName) {
        return getReportPack(file, reportName, true);
    }

    /**
     *
     * @param file
     * @param reportName
     * @param cached
     * @return
     */
    public synchronized ReportPack getReportPack(TFile file, String reportName, boolean cached) {
        if (!cached) {
            return createReportPack(file, reportName);
        }
        Map<String, Reference<ReportPack>> refPacksForRun = getReportPacksForRun(file);

        String units = ConfigData.getDefault().getData(ConfigData.Units);
        Reference<ReportPack> ref = refPacksForRun.get(reportName + "__" + units);

        //get the pack from the ref if the ref is valid
        ReportPack pack = ref != null ? ref.get() : null;

        if (pack == null || !pack.isUpToDate()) {
            pack = createReportPack(file, reportName);

            //store a soft reference to the pack.  A SoftReference is only cleared upon memory demands.
            //TODO: look at more robust caching libraries.  Would be nice to have a timeout.
            ref = new SoftReference<ReportPack>(pack);
            refPacksForRun.put(reportName + "__" + units, ref);
        }
        return pack;
    }

    /**
     *
     * @param file
     * @param reportName
     * @return
     */
    public ReportPack createReportPack(TFile file, String reportName) {
        //create a ReportPack freshness to prevent the checksums from being greedy.
        final FileAgeChecksum fileChecksum = new FileAgeChecksum(file);
        ReportPack.Freshness freshness = new ReportPack.Freshness() {

            @Override
            public boolean isUpToDate() {
                return fileChecksum.isUpToDate();
            }
        };

        ReportPack pack = new ReportPack(getReportLoader(), getConnection(file), reportName, freshness);

        return pack;

    }

    private synchronized Map<String, Reference<ReportPack>> getReportPacksForRun(TFile file) {
        if (c_packs == null) {
            c_packs = new Hashtable<TFile, Map<String, Reference<ReportPack>>>();
        }
        Map<String, Reference<ReportPack>> table = c_packs.get(file);
        if (table == null) {
            table = new Hashtable<String, Reference<ReportPack>>();
            c_packs.put(file, table);
        }
        return table;
    }

    /**
     * Helper function which allows the run directory to be passed as a String.
     * A File object for run directory path is created and passed to
     * display(File, ReportTag).
     *
     * @param file String of the run directory path
     * @param reportName
     */
    public void displayReport(String file, String reportName) {
        if (file == null || file.trim().length() == 0) {
            return;
        }
        displayReport(new TFile(file), reportName);
    }

    /**
     * Primary method for displaying reports. If the requested report is already
     * open it will be brought to the front of the screen. If it is not open it
     * will be created and displayed.
     *
     * @param runFile File object of the run directory
     * @param reportName
     */
    public void displayReport(TFile runFile, String reportName) {
        JFrame frame = null;
        if (runFile == null) {
            frame = createAndDisplayJFrame(null, reportName);
        } else {
            Map<String, JFrame> reportsForRun = getOpenReportsForRun(runFile);
            frame = reportsForRun.get(reportName);
            if (frame == null) {
                //Make a new report frame
                frame = createAndDisplayJFrame(runFile, reportName);
            }
        }

        if (frame == null) {
            return;
        }

        //remove the iconified state bit
        int state = frame.getExtendedState();
        if ((state & JFrame.ICONIFIED) == JFrame.ICONIFIED) {
            state -= JFrame.ICONIFIED;
            frame.setExtendedState(state);
        }
        frame.toFront();
    }

    /**
     *
     * @param reportName
     */
    public void displayReport(String reportName) {
        displayReport((TFile) null, reportName);
    }

    /**
     *
     * @param projectDirectory
     * @param runsLocation
     * @param reportName
     */
    public void displayReport(String projectDirectory, String runsLocation, String reportName) {
        displayReport(new TFile(projectDirectory), new TFile(runsLocation), reportName);
    }

    /**
     * Prompts the user to select an exsisting run and then opens the requested
     * report.
     *
     * @param projectDirectory File object of the project directory
     * @param runsLocation
     * @param reportName
     */
    public void displayReport(TFile projectDirectory, TFile runsLocation, String reportName) {
        String runDir = "";
        WepsFileChooser wfc = new WepsFileChooser(WepsFileChooser.Filetype.RUN, runDir, WepsFileChooser.SELECT);
        wfc.setCurrentDirectory(projectDirectory);
        if (runsLocation != null) {
            wfc.setCurrentDirectory(runsLocation);
        } else {
            wfc.setCurrentDirectory(projectDirectory);
        }

        if (wfc.showDialog(null) == TFileChooser.APPROVE_OPTION) {
            TFile sf = new TFile(wfc.getSelectedFile());
            try {
                runDir = sf.getCanonicalPath();
            } catch (java.io.IOException e) {//System.err.println(
                //         "Error getting canoncial path of runDir");
            }
        } else {
            //System.err.println("No project directory selected");
            return;
        }
        displayReport(runDir, reportName);
    }

    /**
     *
     * @param file
     * @param reportName
     */
    public void generateReportPDF(String file, String reportName) {
        if (file == null || file.trim().length() == 0) {
            return;
        }
        generateReportPDF(new TFile(file), reportName);
    }

    /**
     *
     * @param runFile
     * @param reportName
     */
    public void generateReportPDF(TFile runFile, String reportName) {
        final ReportPack pack = getReportPack(runFile, reportName);
        generateReportPDF(pack, runFile);
    }

    /**
     *
     * @param pack
     * @param pdfDir
     */
    public void generateReportPDF(final ReportPack pack, final TFile pdfDir) {
        if (pack.getPrint() != null) {
            savePDF(pack.getPrint(), new TFile(pdfDir, pack.getReportName() + ".pdf"));
        } else {
            final BlockingBoolean blocking = new BlockingBoolean(true);
            pack.addReportPackListener(new ReportPackListener() {

                @Override
                public void fillError(Throwable t) {
                    //TODO: Report error to user
                    LOGGER.error("Unexpected error filling report.", t);
                    blocking.setBlocking(false);
                }

                @Override
                public void fillCanceled() {
                    //TODO: figure out why?
                    blocking.setBlocking(false);
                }

                @Override
                public void fillFinished(JasperPrint print) {
                    savePDF(print, new TFile(pdfDir, pack.getReportName() + ".pdf"));
                    pack.removeReportPackListener(this);
                    blocking.setBlocking(false);
                }

                @Override
                public void fillStarted() {

                }
            });
            pack.startFill();
            while (blocking.isBlocking()) {
                Thread.yield();
            }
        }
    }

    private static class BlockingBoolean {

        private boolean c_blocking;

        public BlockingBoolean(boolean blocking) {
            setBlocking(blocking);
        }

        public void setBlocking(boolean blocking) {
            synchronized (this) {
                c_blocking = blocking;
            }
        }

        public boolean isBlocking() {
            synchronized (this) {
                return c_blocking;
            }
        }
    }

    private void savePDF(JasperPrint print, TFile pdf) {
        OutputStream out = null;
        try {
            out = new TFileOutputStream(pdf);
            JasperExportManager.exportReportToPdfStream(print, out);
        } catch (JRException jre) {
            LOGGER.error("JasperReports error generating pdf.", jre);
        } catch (FileNotFoundException ioe) {
            LOGGER.error("File IO error generating pdf.", ioe);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException t) {
                    LOGGER.error("Unexpected error while closing stream.", t);
                }
            }
        }
    }

    /**
     * Gets the second layer hashtable for a particular run. If the second layer
     * table does not exist it is created and added to the top level hashtable.
     *
     * @param file File object of the run directory.
     * @return Hashtable <ReportTag, JFrame> containing the reports open for the
     * given run
     */
    private synchronized Map<String, JFrame> getOpenReportsForRun(TFile file) {
        if (c_openReports == null) {
            c_openReports = new Hashtable<TFile, Map<String, JFrame>>();
        }
        Map<String, JFrame> table = c_openReports.get(file);
        if (table == null) {
            table = new Hashtable<String, JFrame>();
            c_openReports.put(file, table);
        }
        return table;
    }

    /**
     * Worker function which actually does the creating displaying of reports. A
     * window listener is added to each report so when closed it is removed from
     * the hashtable of open reports.
     *
     * @param runFile File object of the run directory
     * @param tag ReportTag of the report to open
     * @return JFrame for the created and displayed report
     */
    private JFrame createAndDisplayJFrame(TFile runFile, String reportName) {
        JFrame frame = null;
        switch (reportName) {
            case REPORT_DETAIL:
                DetailReport detailFrame = new DetailReport(runFile);
                ConfigData.getDefault().fireAll(detailFrame);
                detailFrame.display();
                frame = detailFrame;
                frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
                break;
            case REPORT_SOIL:
                RunFileData run = new RunFileData(runFile.getCanOrAbsPath());
                Soil soilFrame = new Soil(run.getData(RunFileData.SoilFile));
                soilFrame.setVisible(true);
                soilFrame.setReadonly(true);
                frame = (JFrame) soilFrame;
                break;
            case REPORT_DEBUG:
                frame = OutputViewer.displayReport(runFile.getCanOrAbsPath());
                break;
            default:
                //ReportViewer
                ReportPack pack = getReportPack(runFile != null ? runFile
                        : new TFile(ConfigData.getDefault().getDataParsed(ConfigData.CurrentProj)), reportName);
                if (runFile == null) {
                    pack = getReportPack(runFile != null ? runFile
                            : new TFile(ConfigData.getDefault().getDataParsed(ConfigData.CurrentProj)), reportName, false);
                }
                ReportViewer viewer = new ReportViewer(pack);
                if (REPORT_RUN.equals(reportName)) {
                    SummaryAccessory notes = new SummaryAccessory(pack, viewer);
                    viewer.addAccessory(notes, BorderLayout.SOUTH);
                    viewer.addReportViewerListener(notes);
                }   //dirty hacks
                String runName = runFile != null ? runFile.getName() : "null";
                String reportTitle = "Report Viewer";
                if (runFile == null) {
                    viewer.runReportButton.setVisible(false);
                    viewer.managementReportButton.setVisible(false);
                    viewer.cropReportButton.setVisible(false);
                    viewer.covercropbutton.setVisible(false);
                    viewer.cropintervalbutton.setVisible(false);
                    viewer.stirReportButton.setVisible(false);
                    viewer.detailReportButton.setVisible(false);
                } else if (REPORT_RUN.equals(reportName)) {
                    reportTitle = "Run";
                    viewer.runReportButton.setEnabled(false);
                } else if (REPORT_MANAGEMENT.equals(reportName)) {
                    reportTitle = "Management";
                    viewer.managementReportButton.setEnabled(false);
                } else if (REPORT_CROPSUM.equals(reportName)) {
                    reportTitle = "Crop Summary";
                    viewer.cropReportButton.setEnabled(false);
                } else if (REPORT_CROPDET.equals(reportName)) {
                    reportTitle = "Crop Detail";
                } //insert cover crop summary and detail
                else if (REPORT_COVERCROPSUM.equals(reportName)) {
                    reportTitle = "Cover Crop Summary";
                    viewer.covercropbutton.setEnabled(false);
                } else if (REPORT_COVERCROPDET.equals(reportName)) {
                    reportTitle = "Cover Crop Detail";
                } //add link for Crop Interval 
                else if (REPORT_CROP_INT_SUM.equals(reportName)) {
                    reportTitle = "Crop Interval Summary";
                    viewer.cropintervalbutton.setEnabled(false);
                } else if (REPORT_CROP_INT_DET.equals(reportName)) {
                    reportTitle = "Crop Inteval Detail";
                } else if (REPORT_CROP_INT_PER_DET.equals(reportName)) {
                    reportTitle = "Crop Inteval Period Detail";
                } else if (REPORT_STIR.equals(reportName)) {
                    reportTitle = "Stir";
                    viewer.stirReportButton.setEnabled(false);
                } else if (REPORT_CONFIDENCE_INTERVAL.equals(reportName)) {
                    reportTitle = "Confidence Interval";
                }
                if (runFile != null) {
                    viewer.setTitle(reportTitle + " : " + runName);
                } else {
                    viewer.setTitle(reportTitle);
                }
                viewer.setVisible(true);
                //size the frame to the screen height      
                if (reportName.equals(REPORT_CROP_INT_SUM) || reportName.equals(REPORT_CROP_INT_DET) || reportName.equals(REPORT_CROP_INT_PER_DET)) {
                    Rectangle screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
                    Dimension size = viewer.getSize();
                    size.height = screen.height < 800 ? screen.height : 800; //975 x 800
                    size.width = screen.width < 975 ? screen.width : 975;
                    viewer.setSize(size);
                    viewer.setLocation(screen.x, screen.y);
                    frame = viewer;
                } else {
                    Rectangle screen = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();
                    int height = screen.height;
                    Dimension size = viewer.getSize();
                    size.height = height > 1250 ? 1250 : height;
                    viewer.setSize(size);
                    viewer.setLocation(screen.x, screen.y);
                    frame = viewer;
                }
        }
        if (frame == null) {
            return null;
        }

        //add a window listener to catch closing windows
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public synchronized void windowClosed(WindowEvent e) {
                Map<String, JFrame> tableToRemoveFrom = null;
                String keyToRemove = null;
                boolean foundTableAndKey = false;
                if (c_openReports == null) {
                    return;
                }
                for (Map<String, JFrame> secondLayer : c_openReports.values()) {
                    for (Map.Entry<String, JFrame> entry : secondLayer.entrySet()) {
                        JFrame frame = entry.getValue();
                        if (frame == e.getSource()) {
                            tableToRemoveFrom = secondLayer;
                            keyToRemove = entry.getKey();
                            foundTableAndKey = true;
                            break;
                        }
                        if (foundTableAndKey) {
                            break;
                        }
                    }
                }
                if (tableToRemoveFrom != null && keyToRemove != null) {
                    tableToRemoveFrom.remove(keyToRemove);
                }
            }
        ;
        });
        
        //cache the frame.
        if (runFile != null) {
            getOpenReportsForRun(runFile).put(reportName, frame);
        }
        return frame;
    }

    /**
     * Returns a cached connection for the given file. Creates a new connection
     * if required.
     *
     * @param file run file directory
     * @return WepsConnection for the run
     * @throws java.lang.NullPointerException
     */
    public WepsConnection getConnection(TFile file) throws NullPointerException {
        //TODO: provide a cache of connections to speed up reporting and reduce memory usage.
        return createConnection(file);
    }

    /**
     * Creates a new instance
     *
     * @param file run file directory
     * @return new instance of a WepsConnection for the run
     * @throws NullPointerException if the file is null.
     */
    public WepsConnection createConnection(TFile file) throws NullPointerException {
        if (file == null) {
            throw new NullPointerException("File is null.");
        }
        try {
            String name = file.getAbsolutePath().substring(file.getAbsolutePath().lastIndexOf(TFile.separator));
            WepsConnection connection = new WepsConnection(name, file);
            connection.setUnits(getConfigData().getData(ConfigData.Units));
            return connection;
        } catch (SQLException se) {
            LOGGER.error("Unable to create weps connection for file: " + file.getAbsolutePath(), se);
        }
        return null;
    }

    private ConfigData getConfigData() {
        return ConfigData.getDefault();
    }

    private ReportLoader getReportLoader() {
        return ReportLoader.getDefault();
    }
}
