package usda.weru.weps.reports;

import de.schlichtherle.truezip.file.TFile;
import de.schlichtherle.truezip.file.TFileInputStream;
import de.schlichtherle.truezip.file.TFileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRTemplate;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.engine.util.JRSaver;
import net.sf.jasperreports.engine.xml.JRXmlTemplateLoader;
import org.apache.log4j.Logger;
import usda.weru.util.About;
import usda.weru.util.ConfigData;

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

    private static final Logger LOGGER = Logger.getLogger(ReportLoader.class);
    private List<ReportProvider> c_providers;
    private ReportCache c_cache;

    private TFile c_install;
    private TFile c_user;
    private String c_classpath;
    private String c_customized;

    private static ReportLoader c_default;

    /**
     *
     */
    public ReportLoader() {
        TFile ireportFile = new TFile(About.getUserWeps(), "ireport.properties");
        Properties props = new Properties();
        TFileInputStream in = null;
        try {
            in = new TFileInputStream(ireportFile);
            props.load(in);
            TFile install = new TFile(props.getProperty("reports"));
            c_install = install;
        } catch (IOException ioe) {
            LOGGER.error("File IO Error.", ioe);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    LOGGER.error("Unknown error.", e);
                }
            }
        }
    }

    private ReportLoader(TFile install, TFile user, String classpath) {
        c_install = install;
        c_user = user;
        c_classpath = classpath;
    }

    /**
     *
     * @return
     */
    public synchronized static ReportLoader getDefault() {
        if (c_default == null) {
            c_default = new ReportLoader(null, null, "/usda/weru/weps/reports");
        }
        return c_default;
    }

    /**
     *
     * @return
     */
    public String[] getReportNames() {
        synchronized (this) {
            if (c_providers == null) {
                initProviders();
            }
        }
        List<String> reportNames = new LinkedList<String>();
        for (ReportProvider provider : c_providers) {
            for (String reportName : provider.getReportNames()) {
                if (!reportNames.contains(reportName)) {
                    reportNames.add(reportName);
                }
            }
        }
        return reportNames.toArray(new String[reportNames.size()]);
    }

    /**
     *
     * @param reportName
     * @return
     */
    public String getReportDisplayName(String reportName) {
        JasperReport report = getReport(reportName);
        if (report != null) {
            return report.getName();
        } else {
            return reportName;
        }
    }

    /**
     *
     * @param templateName
     * @return
     */
    public JRTemplate getTemplate(String templateName) {
        return getTemplate(templateName, false);
    }

    private JRTemplate getTemplate(String templateName, boolean customized) {
        //this allows styles to be customized
        if (c_customized != null && !customized) {
            JRTemplate customizedTemplate = getTemplate(c_customized + "_" + templateName, true);
            if (customizedTemplate != null) {
                return customizedTemplate;
            }
        }

        //Load the compiled report obejct
        String path = c_classpath + "/" + templateName + ".jrtx";
        InputStream in = null;
        try {
            in = ReportLoader.class.getResourceAsStream(path);
            if (in == null) {
                //no template found.
                return null;
            }
            JRTemplate result = JRXmlTemplateLoader.load(in);

            if (result != null) {
                return result;
            } else {
                LOGGER.error("Loaded object isn't a JRTemplate: " + path);
                return null;
            }
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ioe) {
                    LOGGER.error("Error closing file: " + path, ioe);
                }
            }
        }
    }

    /**
     *
     * @param reportName
     * @return
     */
    public JasperReport getReport(String reportName) {
        return getReport(reportName, false);
    }

    private JasperReport getReport(String reportName, boolean customized) {
        //this allows sub reports to be customized
        if (c_customized != null && !customized) {
            JasperReport customizedReport = getReport(c_customized + reportName, true);
            if (customizedReport != null) {
                return customizedReport;
            }
        }

        LOGGER.debug("Loading report: " + reportName);

        synchronized (this) {
            if (c_providers == null) {
                initProviders();
            }
        }

        ReportProvider newest = null;
        for (ReportProvider provider : c_providers) {
            if (newest == null) {
                newest = provider;
                continue;
            }
            if (provider.getReportTimestamp(reportName) > newest.getReportTimestamp(reportName)) {
                newest = provider;
            }
        }

        if (newest != null) {
            long timestamp = System.currentTimeMillis();
            JasperReport report = newest.getReport(reportName);
            c_cache.cacheReport(reportName, report, timestamp);
            return report;
        } else {
            return null;
        }
    }

    /**
     *
     */
    protected void initProviders() {
        c_providers = new LinkedList<ReportProvider>();

        //the cache is the first provider
        c_cache = new ReportCache();
        c_providers.add(c_cache);

        //do have a customized override?
        c_customized = ConfigData.getDefault().getDataParsed(ConfigData.ReportsCustomized);
        if (c_customized != null && c_customized.trim().length() == 0) {
            c_customized = null;
        }

        //user reports
        if (c_user == null && c_install != null) {
            c_user = new TFile(About.getUserWeps(), "reports");
        }
        FileReportProvider userReports = new FileReportProvider(c_user);
        c_providers.add(userReports);

        //add a provider for the install directory, reports are compiled to the ${user.home}/.weps/reports directory
        FileReportProvider installReports = new FileReportProvider(c_install, c_user);
        c_providers.add(installReports);

        //add classpath provider
        if (c_classpath != null) {
            ClassPathReportProvider cpReports = new ClassPathReportProvider(c_classpath);
            c_providers.add(cpReports);
        }
    }

    /**
     *
     */
    public interface ReportProvider {

        /**
         *
         * @param reportName
         * @return
         */
        public long getReportTimestamp(String reportName);

        /**
         *
         * @param reportName
         * @return
         */
        public JasperReport getReport(String reportName);

        /**
         *
         * @return
         */
        public String[] getReportNames();
    }

    private class ReportCache implements ReportProvider {

        protected Map<String, Reference<Entry>> c_cache;

        public ReportCache() {
            c_cache = new HashMap<String, Reference<Entry>>();
        }

        @Override
        public long getReportTimestamp(String reportName) {
            Reference<Entry> ref = c_cache.get(reportName);
            Entry entry = ref != null ? ref.get() : null;

            return entry != null ? entry.getTimestamp() : 0;
        }

        @Override
        public JasperReport getReport(String reportName) {
            Reference<Entry> ref = c_cache.get(reportName);
            Entry entry = ref != null ? ref.get() : null;

            return entry != null ? entry.getReport() : null;
        }

        public void cacheReport(String reportName, JasperReport report, Long timestamp) {
            if (report != null) {
                Entry entry = new Entry(report, timestamp);
                c_cache.put(reportName, new SoftReference<Entry>(entry));
            } else {
                c_cache.remove(reportName);
            }
        }

        @Override
        public String[] getReportNames() {
            return c_cache.keySet().toArray(new String[c_cache.size()]);
        }

        private class Entry {

            private final JasperReport c_report;
            private final Long c_timestamp;

            public Entry(JasperReport report, Long timestamp) {
                c_report = report;
                c_timestamp = timestamp;
            }

            public JasperReport getReport() {
                return c_report;
            }

            public Long getTimestamp() {
                return c_timestamp != null ? c_timestamp : 0;
            }

        }
    }

    private class ClassPathReportProvider implements ReportProvider {

        private String c_classpath;

        public ClassPathReportProvider(String classpath) {
            c_classpath = classpath;
        }

        @Override
        public long getReportTimestamp(String reportName) {
            String path = c_classpath + "/" + reportName + ".jasper";
            InputStream in = ReportLoader.class.getResourceAsStream(path);
            //TODO: return the build date
            return in != null ? 5000 : -1;
        }

        @Override
        public JasperReport getReport(String reportName) {
            //Load the compiled report obejct
            String path = c_classpath + "/" + reportName + ".jasper";
            InputStream in = null;
            try {
                in = ReportLoader.class.getResourceAsStream(path);
                if (in == null) {
                    //no report found.
                    return null;
                }
                Object o = JRLoader.loadObject(in);
                if (o instanceof JasperReport) {
                    return (JasperReport) o;
                } else {
                    LOGGER.error("Loaded object isn't a JasperReport: " + path);
                    return null;
                }
            } catch (JRException jre) {
                LOGGER.error("Error loading compiled report: " + path, jre);
                //TODO: Maybe fall back to the source and recompile?
                return null;
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException ioe) {
                        LOGGER.error("Error closing file: " + path, ioe);
                    }
                }
            }
        }

        @Override
        public String[] getReportNames() {
            return new String[]{"run", "management", "crop", "stir"};
        }

    }

    private class FileReportProvider implements ReportProvider {

        private TFile c_dir;
        private TFile c_compileDir;
        private static final String EXT_COMPILED = ".jasper";
        private static final String EXT_SOURCE = ".jrxml";

        public FileReportProvider(TFile dir, TFile compileDir) {
            c_dir = dir;
            c_compileDir = compileDir;
        }

        public FileReportProvider(TFile dir, boolean saveCompiled) {
            this(dir, saveCompiled ? dir : null);
        }

        public FileReportProvider(TFile dir) {
            this(dir, true);
        }

        @Override
        public long getReportTimestamp(String reportName) {
            TFile sourceFile = new TFile(c_dir, reportName + EXT_SOURCE);
            TFile compiledFile = new TFile(c_dir, reportName + EXT_COMPILED);

            long timestamp = Math.max(sourceFile != null ? sourceFile.lastModified() : 0,
                    compiledFile != null ? compiledFile.lastModified() : 0);

            return timestamp;
        }

        @Override
        public JasperReport getReport(String reportName) {
            TFile sourceFile = new TFile(c_dir, reportName + EXT_SOURCE);
            TFile compiledFile = new TFile(c_dir, reportName + EXT_COMPILED);

            //Temp will be the file to use
            TFile temp = getNewestFile(compiledFile, sourceFile);

            if (temp == null) {
                return null;
            }

            if (temp.getName().endsWith(EXT_COMPILED)) {
                //Load the compiled report obejct
                TFileInputStream in = null;
                try {
                    in = new TFileInputStream(temp);
                    Object o = JRLoader.loadObject(in);
                    if (o instanceof JasperReport) {
                        return (JasperReport) o;
                    } else {
                        LOGGER.error("Loaded object isn't a JasperReport: " + temp.getAbsolutePath());
                        return null;
                    }
                } catch (FileNotFoundException | JRException ioe) {
                    LOGGER.error("Error loading compiled report: " + temp.getAbsolutePath(), ioe);
                    //TODO: Maybe fall back to the source and recompile?
                    return null;
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException ioe) {
                            LOGGER.error("Error closing file: " + temp.getAbsolutePath(), ioe);
                        }
                    }
                }
            } else if (temp.getName().endsWith(EXT_SOURCE)) {
                //Compile the report
                TFileInputStream in = null;
                TFileOutputStream out = null;
                try {
                    in = new TFileInputStream(sourceFile);

                    JasperReport report = JasperCompileManager.compileReport(in);

                    if (c_compileDir != null) {
                        //save the report
                        TFile newFile = new TFile(c_compileDir, compiledFile.getName());
                        newFile.getParentFile().mkdirs();
                        out = new TFileOutputStream(newFile);
                        JRSaver.saveObject(report, out);
                    }

                    return report;

                } catch (FileNotFoundException | JRException ioe) {
                    LOGGER.error("Error compiling report: " + temp.getAbsolutePath(), ioe);
                    //TODO: Maybe fall back to the source and recompile?
                    return null;
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException ioe) {
                            LOGGER.error("Error closing file: " + temp.getAbsolutePath(), ioe);
                        }
                    }
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException ioe) {
                            LOGGER.error("Error closing file: " + compiledFile.getAbsolutePath(), ioe);
                        }
                    }
                }

            } else {
                return null;
            }
        }

        private TFile getNewestFile(TFile fileA, TFile fileB) {
            long aAge = fileA != null ? fileA.lastModified() : 0;
            long bAge = fileB != null ? fileB.lastModified() : 0;

            if (aAge >= bAge) {
                return fileA;
            } else {
                return fileB;
            }
        }

        @Override
        public String[] getReportNames() {
            List<String> names = new LinkedList<String>();

            for (TFile file : c_dir.listFiles(c_dir.getArchiveDetector())) {
                String name = file.getName();
                if (name.endsWith(EXT_SOURCE)) {
                    names.add(name.substring(0, name.length() - EXT_SOURCE.length()));
                } else if (name.endsWith(EXT_COMPILED)) {
                    names.add(name.substring(0, name.length() - EXT_COMPILED.length()));
                }
            }

            return names.toArray(new String[names.size()]);
        }

    }

}
