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.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 usda.weru.soil.Soil;
import usda.weru.util.About;
import usda.weru.util.ConfigData;
import usda.weru.util.FileAgeChecksum;
import usda.weru.util.wepsFileChooser2.WepsFileChooser2;
import usda.weru.util.wepsFileChooser2.WepsFileTypes2;
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;
    private  static int customQuickplot = 0; // default
    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";
    public static final String REPORT_CROPDET = "cropdetail";
    public static final String REPORT_COVERCROPSUM = "covercropsummary";
    public static final String REPORT_COVERCROPDET = "covercropdetail";
    public static final String REPORT_CROP_INT_SUM = "cropintervalsum";
    public static final String REPORT_CROP_INT_DET = "cropintervaldetail";
    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() {
        c_packs = null;
    }
    
//    @Override
//    public void finalize() throws Throwable {
//        super.finalize();
//    }
    
    public void close() {
        if (c_packs != null) {
            c_packs = null;
        }
        c_default = null;        
    }
    
    public synchronized static ReportManager getDefault() {
        if (c_default == null) {
            c_default = new ReportManager();
        }
        return c_default;
    }
    public synchronized ReportPack getReportPack(TFile file, String reportName) {
        return getReportPack(file, reportName, true, null);
    }

    public synchronized ReportPack getReportPack(TFile file, String reportName, boolean cached) {
        return getReportPack(file, reportName, cached, null);
    }
    
    public synchronized ReportPack getReportPack(TFile file, String reportName, boolean cached, RunFileData rfd) {
        if (!cached) {
            return createReportPack(file, reportName, rfd);
        }
        
// MEH: using this map causes memory leaks.
//      If speed is issue, need more robust handling
//        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;
        ReportPack pack = null;

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

            //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;
    }

    public ReportPack createReportPack(TFile file, String reportName, RunFileData rfd) {
        //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 = null;
        pack = new ReportPack(getReportLoader(), getConnection(file, rfd), 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;
    }


    public void displayReport(String file, String reportName) {
        if (file == null || file.trim().length() == 0) {
            return;
        }
        displayReport(new TFile(file), reportName, null);
    }
    
    public void displayReport(String file, String reportName, RunFileData rfd) {
        if (file == null || file.trim().length() == 0) {
            return;
        }
        displayReport(new TFile(file), reportName, rfd);
    }

    public void displayReport(TFile runFile, String reportName, int custom) {
        customQuickplot = custom;
        displayReport(runFile, reportName);
    }

    public void displayReport(TFile runFile, String reportName) {
        displayReport (runFile, reportName, null);
    }
    
    public void displayReport(TFile runFile, String reportName, RunFileData rfd) {
        JFrame frame = null;
        if (runFile == null) {
            frame = createAndDisplayJFrame(null, reportName, rfd);
        } else {
            Map<String, JFrame> reportsForRun = getOpenReportsForRun(runFile);
            frame = reportsForRun.get(reportName);
            if (frame == null) {
                //Make a new report frame
                frame = createAndDisplayJFrame(runFile, reportName, rfd);
            }
        }

        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();
    }

    public void displayReport(String reportName) {
        displayReport((TFile) null, reportName);
    }

    // Helper method for constructing different pre set quickplots
    public void displayReport(String file, String reportName, int custom) {
        customQuickplot = custom;
        displayReport(file, reportName);
    }
    
    // helping method for constructing pre set quickplots
    public void displayReport(String projectDirectory, String runsLocation, String reportName, int custom) {
        customQuickplot = custom;
        displayReport(projectDirectory, runsLocation, reportName);
    }

    public void displayReport(String projectDirectory, String runsLocation, String reportName) {
        displayReport(new TFile(projectDirectory), new TFile(runsLocation), reportName);
    }

    /**
     * Prompts the user to select an existing 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 = "";
        WepsFileChooser2 wfc = new WepsFileChooser2(WepsFileTypes2.Run, runDir, WepsFileChooser2.Action.Select);
        wfc.setCurrentDirectory(projectDirectory.getAbsolutePath());
        if (runsLocation != null) {
            wfc.setCurrentDirectory(runsLocation.getAbsolutePath());
        }

        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);
    }

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

    public void generateReportPDF(String file, String reportName, RunFileData rfd) {
        if (file == null || file.trim().length() == 0) {
            return;
        }
        generateReportPDF(new TFile(file), reportName,  rfd);
    }

    public void generateReportPDF(TFile runFile, String reportName, RunFileData rfd) {
        final ReportPack pack = getReportPack(runFile, reportName, true, rfd);
        generateReportPDF(pack, runFile);
    }

    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);
                    pack.removeReportPackListener(this);
                    blocking.setBlocking(false);
                }

                @Override
                public void fillCanceled() {
                    //TODO: figure out why?
                    pack.removeReportPackListener(this);
                    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();
            }
            // helping w/ garbage collection
            pack.close();
        }
    }

    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, RunFileData rfd) {
        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); // This sets it to full screen
                frame.setExtendedState(JFrame.NORMAL);
                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;
            case REPORT_QUICKPLOT:
                DetailReport quickplot_er = new DetailReport(runFile);
                ConfigData.getDefault().fireAll(quickplot_er);
                quickplot_er.justInitQuickplot(customQuickplot);
                break; //Doesn't display detail report
            default:
                //ReportViewer
                ReportPack pack = getReportPack(runFile != null ? 
                                                runFile : new TFile(ConfigData.getDefault().getDataParsed(ConfigData.CurrentProj))
                                                , reportName, false, rfd);
//                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;
        }
        
        frame.setIconImage (About.getWeruIconImage());
        

        //add a window listener to catch closing windows
        frame.addWindowListener(new WindowAdapter() {
            Map<TFile, Map<String, JFrame>> openReports=c_openReports;
            
            @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);
                    
                    TFile[] keys = openReports.keySet().toArray(new TFile[0]);
                    for (TFile f : keys) {
                        if (openReports.get(f) == tableToRemoveFrom) {
                            openReports.remove(f);
                        }
                   }
                }
//                //frame.getListeners(listenerType)
//                Object frame = e.getSource();
//                if (frame instanceof ReportViewer) {
//                    ((ReportViewer)frame).removeAllListeners();
//                }
                System.gc();
            }
        ;
        });
        
//        //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, null);
    }
    
    public WepsConnection getConnection(TFile file, RunFileData rfd) throws NullPointerException {
        //TODO: provide a cache of connections to speed up reporting and reduce memory usage.
        return createConnection(file, rfd);
    }

    public WepsConnection createConnection(TFile file, RunFileData rfd) throws NullPointerException {
        if (file == null) {
            throw new NullPointerException("File is null.");
        }
        try {
            String name = file.getAbsolutePath().substring(file.getAbsolutePath().lastIndexOf(TFile.separator));
//            WepsConnection connection = (rfd != null) ? 
//                                        new WepsConnection(name, rfd) : new WepsConnection(name, file);
            WepsConnection connection = new WepsConnection(name, rfd, 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();
    }
}
