/*******************************************************************************
 * This file is part of OpenNMS(R).
 *
 * Copyright (C) 2006-2011 The OpenNMS Group, Inc.
 * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
 *
 * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
 *
 * OpenNMS(R) is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * OpenNMS(R) is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with OpenNMS(R).  If not, see:
 *      http://www.gnu.org/licenses/
 *
 * For more information contact:
 *     OpenNMS(R) Licensing <license@opennms.org>
 *     http://www.opennms.org/
 *     http://www.opennms.com/
 *******************************************************************************/

package org.opennms.netmgt.config;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.ValidationException;
import org.opennms.core.utils.ConfigFileConstants;
import org.opennms.core.xml.CastorUtils;
import org.opennms.netmgt.config.kscReports.Report;
import org.opennms.netmgt.config.kscReports.ReportsList;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.Assert;

/**
 * <p>KSC_PerformanceReportFactory class.</p>
 *
 * @author ranger
 * @version $Id: $
 */
public class KSC_PerformanceReportFactory {
    /**
     * The static singleton instance object.
     * Null if init() hasn't been successfully called.
     */
    private static KSC_PerformanceReportFactory s_instance = null;

    /** File name of the KSC_PerformanceReport.xml */
    private static File s_configFile = null;

    /** An instance of the ReportsList configuration */
    private ReportsList m_config;

    /**
     * The array of values that may be used in the timespan declaration of a
     * graph
     */
    public static final String[] TIMESPAN_OPTIONS = {
            "1_hour",
            "2_hour",
            "4_hour",
            "6_hour",
            "8_hour",
            "12_hour",
            "1_day",
            "2_day",
            "7_day",
            "1_month",
            "3_month",
            "6_month",
            "1_year",
            "Today",
            "Yesterday",
            "Yesterday 9am-5pm",
            "Yesterday 5pm-10pm",
            "This Week",
            "Last Week",
            "This Month",
            "Last Month",
            "This Quarter",
            "Last Quarter",
            "This Year",
            "Last Year"
    };

    /**
     * Map of all Reports by their ID, ordered based on their order in the config file.
     */
    private Map<Integer, Report> m_reportList;

    /**
     * Empty Private Constructor. Cannot be instantiated outside itself.
     */
    private KSC_PerformanceReportFactory() {
    }

    /**
     * Init routine. Must be called before calling getInstance() to instantiate *
     *
     * @throws java.io.IOException if any.
     * @throws java.io.FileNotFoundException if any.
     * @throws org.exolab.castor.xml.MarshalException if any.
     * @throws org.exolab.castor.xml.ValidationException if any.
     */
    public static synchronized void init() throws IOException, FileNotFoundException, MarshalException, ValidationException {
        if (isInitialized()) {
            return;
        }
        
        KSC_PerformanceReportFactory newInstance = new KSC_PerformanceReportFactory();
        newInstance.reload();
        
        s_instance = newInstance;
    }

    /**
     * Singleton static call to get the only instance that should exist for the
     * KSC_PerformanceReportFactory
     *
     * @return the single KSC_PerformanceReportFactory instance
     * @throws java.lang.IllegalStateException if any.
     */
    public static synchronized KSC_PerformanceReportFactory getInstance() throws IllegalStateException {
        assertInitialized();

        return s_instance;
    }

    /**
     * Parses the KSC_PerformanceReport.xml via the Castor classes.
     *
     * @throws java.io.IOException if any.
     * @throws java.io.FileNotFoundException if any.
     * @throws org.exolab.castor.xml.MarshalException if any.
     * @throws org.exolab.castor.xml.ValidationException if any.
     */
    public synchronized void reload() throws IOException, FileNotFoundException, MarshalException, ValidationException {
        if (s_configFile == null) s_configFile = ConfigFileConstants.getFile(ConfigFileConstants.KSC_REPORT_FILE_NAME);

        m_config = CastorUtils.unmarshal(ReportsList.class, new FileSystemResource(s_configFile));
        
        setIdsOnAllReports();
        
        m_reportList = createReportList();
    }

    public static void setConfigFile(final File configFile) {
        s_configFile = configFile;
    }
    
    private void setIdsOnAllReports() {
        int i = 0;

        // Make sure that i is larger than the highest report ID
        for (Report report : m_config.getReportCollection()) {
            if (report.hasId() && report.getId() >= i) {
                i = report.getId() + 1;
            }
        }
        
        // Set IDs for any report lacking one.
        for (Report report : m_config.getReportCollection()) {
            if (!report.hasId()) {
                report.setId(i);
                i++;
            }
        }
    }

    /**
     * Saves the KSC_PerformanceReport.xml data.
     *
     * @throws java.io.IOException if any.
     * @throws java.io.FileNotFoundException if any.
     * @throws org.exolab.castor.xml.MarshalException if any.
     * @throws org.exolab.castor.xml.ValidationException if any.
     */
    public synchronized void saveCurrent() throws IOException, FileNotFoundException, MarshalException, ValidationException {
        assertInitialized();

        sortByTitle();
        
        CastorUtils.marshalViaString(m_config, s_configFile);
        
        reload();
    }

    private static void assertInitialized() {
        Assert.state(isInitialized(), "KSC_PerformanceReportFactory.init() has not been called");
    }

    private static boolean isInitialized() {
        return s_instance != null;
    }

    /**
     * Sorts the Reports List by their title.
     */
    public void sortByTitle() {
        Arrays.sort(m_config.getReport(), new Comparator<Report>() {
            public int compare(Report o1, Report o2) {
                return o1.getTitle().compareTo(o2.getTitle());
            }
        });
    }
    
    /**
     * <p>getReportByIndex</p>
     *
     * @param index a int.
     * @return a {@link org.opennms.netmgt.config.kscReports.Report} object.
     */
    public Report getReportByIndex(int index) {
        return m_reportList.get(index);
    }
    
    private Map<Integer, Report> createReportList() {
        Map<Integer, Report> reports = new LinkedHashMap<Integer, Report>(m_config.getReportCount());

        for (Report report : m_config.getReportCollection()) {
            if (reports.containsKey(report.getId())) {
                throw new IllegalArgumentException("Report id " + report.getId() + " is used by multiple reports in configuration file");
            }
            reports.put(report.getId(), report);
        }
        
        return reports;
    }

    /**
     * <p>getReportList</p>
     *
     * @return a {@link java.util.Map} object.
     */
    public Map<Integer, String> getReportList() {
        LinkedHashMap<Integer, String> reports = new LinkedHashMap<Integer, String>(m_config.getReportCount());

        List<Report> reportList = m_config.getReportCollection();
        Collections.sort(reportList, new Comparator<Report>() {
            public int compare(Report o1, Report o2) {
                return o1.getTitle().compareTo(o2.getTitle());
            }
        });
        
        for (Report report : reportList) {
            reports.put(report.getId(), report.getTitle());
        }
        
        return Collections.unmodifiableMap(reports);
    }

    /**
     * Deletes the indexed report and updates file configuration
     *
     * @param index a int.
     * @throws java.lang.ArrayIndexOutOfBoundsException if any.
     * @throws java.io.IOException if any.
     * @throws java.io.FileNotFoundException if any.
     * @throws org.exolab.castor.xml.MarshalException if any.
     * @throws org.exolab.castor.xml.ValidationException if any.
     */
    public void deleteReportAndSave(int index) throws ArrayIndexOutOfBoundsException, IOException, FileNotFoundException, MarshalException, ValidationException {
        Report report = getReportByIndex(index);
        if (report == null) {
            throw new ArrayIndexOutOfBoundsException("Reports List index to be deleted is out of bounds: " + index);
        }

        m_config.removeReport(report);
        saveCurrent();
    }

    public void addReport(Report report) {
        m_config.addReport(report);
        setIdsOnAllReports();
    }

    public void setReport(int index, Report report) {
        int arrayIndex = getArrayIndex(index);
        if (arrayIndex == -1) {
            throw new IllegalArgumentException("Could not find report with ID of " + index);
        }
        
        // Make sure we preserve the existing ID, if it exists (which it should)
        if (m_config.getReport(arrayIndex).hasId()) {
            report.setId(m_config.getReport(arrayIndex).getId());
        }
        
        m_config.setReport(arrayIndex, report);
        setIdsOnAllReports();
    }
    
    private int getArrayIndex(int index) {
        int i = 0;
        for (Report report : m_config.getReportCollection()) {
            if (report.getId() == index) {
                return i;
            }
            
            i++;
        }
        
        return -1;
    }
    
    /**
     * This method requires begin time and end time to be set to the current
     * time prior to call. The start and stop times are relative to this time.
     * Init values as follows: begin_time = Calendar.getInstance(); end_time =
     * Calendar.getInstance();
     *
     * @param interval a {@link java.lang.String} object.
     * @param begin_time a {@link java.util.Calendar} object.
     * @param end_time a {@link java.util.Calendar} object.
     * @throws java.lang.IllegalArgumentException if any.
     */
    public static synchronized void getBeginEndTime(String interval, Calendar begin_time, Calendar end_time) throws IllegalArgumentException {
        if (interval.equals("1_hour")) {
            begin_time.add(Calendar.HOUR, -1);
        } else if (interval.equals("2_hour")) {
            begin_time.add(Calendar.HOUR, -2);
        } else if (interval.equals("4_hour")) {
            begin_time.add(Calendar.HOUR, -4);
        } else if (interval.equals("6_hour")) {
            begin_time.add(Calendar.HOUR, -6);
        } else if (interval.equals("8_hour")) {
            begin_time.add(Calendar.HOUR, -8);
        } else if (interval.equals("12_hour")) {
            begin_time.add(Calendar.HOUR, -12);
        } else if (interval.equals("1_day")) {
            begin_time.add(Calendar.DATE, -1);
        } else if (interval.equals("2_day")) {
            begin_time.add(Calendar.DATE, -2);
        } else if (interval.equals("7_day")) {
            begin_time.add(Calendar.DATE, -7);
        } else if (interval.equals("1_month")) {
            begin_time.add(Calendar.DATE, -30);
        } else if (interval.equals("3_month")) {
            begin_time.add(Calendar.DATE, -90);
        } else if (interval.equals("6_month")) {
            begin_time.add(Calendar.DATE, -183);
        } else if (interval.equals("1_year")) {
            begin_time.add(Calendar.DATE, -365);
        } else {
            // From current time, lets zero out the small components
            begin_time.set(Calendar.HOUR_OF_DAY, 0);
            begin_time.set(Calendar.MINUTE, 0);
            begin_time.set(Calendar.SECOND, 0);
            end_time.set(Calendar.HOUR_OF_DAY, 0);
            end_time.set(Calendar.MINUTE, 0);
            end_time.set(Calendar.SECOND, 0);

            if (interval.equals("Today")) {
                end_time.add(Calendar.DATE, 1);
            } else if (interval.equals("Yesterday")) {
                begin_time.add(Calendar.DATE, -1);
            } else if (interval.equals("Yesterday 9am-5pm")) {
                begin_time.add(Calendar.DATE, -1);
                begin_time.set(Calendar.HOUR_OF_DAY, 9);
                end_time.add(Calendar.DATE, -1);
                end_time.set(Calendar.HOUR_OF_DAY, 17);
            } else if (interval.equals("Yesterday 5pm-10pm")) {
                begin_time.add(Calendar.DATE, -1);
                begin_time.set(Calendar.HOUR_OF_DAY, 17);
                end_time.add(Calendar.DATE, -1);
                end_time.set(Calendar.HOUR_OF_DAY, 22);
            } else if (interval.equals("This Week") || interval.equals("Last Week")) {
                begin_time.set(Calendar.DAY_OF_WEEK, 1);
                end_time.set(Calendar.DAY_OF_WEEK, 7);
                end_time.set(Calendar.HOUR_OF_DAY, 23);
                end_time.set(Calendar.MINUTE, 59);
                if (interval.equals("Last Week")) {
                    begin_time.add(Calendar.DATE, -7);
                    end_time.add(Calendar.DATE, -7);
                }
            } else if (interval.equals("This Month")) {
                begin_time.set(Calendar.DATE, 1);
                end_time.add(Calendar.MONTH, 1);
                end_time.set(Calendar.DATE, 1);
            } else if (interval.equals("Last Month")) {
                begin_time.add(Calendar.MONTH, -1);
                begin_time.set(Calendar.DATE, 1);
                end_time.set(Calendar.DATE, 1);
            } else if (interval.equals("This Quarter") || interval.equals("Last Quarter")) {
                begin_time.set(Calendar.DATE, 1);
                end_time.set(Calendar.DATE, 1);

                switch (begin_time.get(Calendar.MONTH)) {
                case 0:
                case 1:
                case 2:
                    begin_time.set(Calendar.MONTH, 0);
                    end_time.set(Calendar.MONTH, 3);
                    break;
                case 3:
                case 4:
                case 5:
                    begin_time.set(Calendar.MONTH, 3);
                    end_time.set(Calendar.MONTH, 6);
                    break;
                case 6:
                case 7:
                case 8:
                    begin_time.set(Calendar.MONTH, 6);
                    end_time.set(Calendar.MONTH, 9);
                    break;
                case 9:
                case 10:
                case 11:
                    begin_time.set(Calendar.MONTH, 9);
                    end_time.set(Calendar.MONTH, 0);
                    end_time.add(Calendar.YEAR, 1);
                    break;
                default:
                    throw new IllegalArgumentException("Invalid Calendar Month " + begin_time.get(Calendar.MONTH));
                }
                if (interval.equals("Last Quarter")) {
                    begin_time.add(Calendar.MONTH, -3);
                    end_time.add(Calendar.MONTH, -3);
                }
            } else if (interval.equals("This Year")) {
                begin_time.set(Calendar.MONTH, 0);
                begin_time.set(Calendar.DATE, 1);
                end_time.set(Calendar.MONTH, 0);
                end_time.set(Calendar.DATE, 1);
                end_time.add(Calendar.YEAR, 1);
            } else if (interval.equals("Last Year")) {
                begin_time.set(Calendar.MONTH, 0);
                begin_time.set(Calendar.DATE, 1);
                begin_time.add(Calendar.YEAR, -1);
                end_time.set(Calendar.MONTH, 0);
                end_time.set(Calendar.DATE, 1);
            } else {
                throw new IllegalArgumentException("Unknown graph timespan: " + interval);
            }
        }
    }
}
