package com.tagtraum.perf.gcviewer;

import com.tagtraum.perf.gcviewer.math.DoubleData;
import com.tagtraum.perf.gcviewer.math.IntData;
import com.tagtraum.perf.gcviewer.math.RegressionLine;

import java.io.File;
import java.io.Serializable;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.net.URL;
import java.net.URLConnection;
import java.net.HttpURLConnection;

/**
 * Collection of GCEvents.
 * <p/>
 * Date: Jan 30, 2002
 * Time: 5:01:45 PM
 *
 * @author <a href="mailto:hs@tagtraum.com">Hendrik Schreiber</a>
 * @version $Id: $
 */
public class GCModel implements Serializable {

    private static Logger LOG = Logger.getLogger(GCModel.class.getName());


    private List allEvents;
    private List events;
    private List gcEvents;
    private List concurrentGCEvents;
    private List currentNoFullGCEvents;
    private List fullGCEvents;
    private long lastModified;
    private long length;

    private long footprint;
    private double runningTime;
    private DoubleData pause;
    private DoubleData fullGCPause;
    private DoubleData gcPause;
    private long freedMemory;
    private Format format;
    private IntData postGCUsedMemory;
    private IntData postFullGCUsedMemory;
    private IntData freedMemoryByGC;
    private IntData freedMemoryByFullGC;
    private DoubleData postGCSlope;
    private RegressionLine currentPostGCSlope;
    private RegressionLine currentRelativePostGCIncrease;
    private DoubleData relativePostGCIncrease;
    private RegressionLine postFullGCSlope;
    private RegressionLine relativePostFullGCIncrease;
    private boolean countTenuredAsFull = true;
    private URL url;

    public GCModel(boolean countTenuredAsFull) {
        this.countTenuredAsFull = countTenuredAsFull;
        this.allEvents = new ArrayList();
        this.events = new ArrayList();
        this.gcEvents = new ArrayList();
        this.concurrentGCEvents = new ArrayList();
        this.fullGCEvents = new ArrayList();
        this.currentNoFullGCEvents = new ArrayList();
        this.currentPostGCSlope = new RegressionLine();
        this.postFullGCSlope = new RegressionLine();
        this.postGCSlope = new DoubleData();
        this.freedMemoryByGC = new IntData();
        this.freedMemoryByFullGC = new IntData();
        this.postFullGCUsedMemory = new IntData();
        this.postGCUsedMemory = new IntData();
        this.pause = new DoubleData();
        this.fullGCPause = new DoubleData();
        this.gcPause = new DoubleData();
        this.currentRelativePostGCIncrease = new RegressionLine();
        this.relativePostGCIncrease = new DoubleData();
        this.relativePostFullGCIncrease = new RegressionLine();
    }

    public boolean isCountTenuredAsFull() {
        return countTenuredAsFull;
    }

    public long getLastModified() {
        return lastModified;
    }

    public URL getURL() {
        return url;
    }

    public void setURL(final URL url) {
        this.url = url;
        if (url.getProtocol().startsWith("http")) {
            HttpURLConnection urlConnection = null;
            try {
                urlConnection = (HttpURLConnection)url.openConnection();
                urlConnection.setRequestMethod("HEAD");
                this.length = urlConnection.getContentLength();
                this.lastModified = urlConnection.getLastModified();
            } catch (IOException e) {
                if (LOG.isLoggable(Level.WARNING)) LOG.log(Level.WARNING, "Failed to obtain age and length of URL " + url, e);
            } finally {
                try {
                    if (urlConnection != null) {
                        final InputStream inputStream = urlConnection.getInputStream();
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException ignore) {
                                // ignore
                            }
                        }
                    }
                } catch (IOException ignore) {
                    // ignore
                }
            }
        }
        else {
            URLConnection urlConnection = null;
            try {
                urlConnection = url.openConnection();
                this.length = urlConnection.getContentLength();
                this.lastModified = urlConnection.getLastModified();
            } catch (IOException e) {
                if (LOG.isLoggable(Level.WARNING)) LOG.log(Level.WARNING, "Failed to obtain age and length of URL " + url, e);
            } finally {
                try {
                    if (urlConnection != null) {
                        final InputStream inputStream = urlConnection.getInputStream();
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException ignore) {
                                // ignore
                            }
                        }
                    }
                } catch (IOException ignore) {
                    // ignore
                }
            }
        }
    }

    public boolean isDifferent(File otherFile) {
        // we just ignore the file name for now...
        return this.lastModified != otherFile.lastModified() || this.length != otherFile.length();
    }

    public boolean isDifferent(URL otherURL) {
        long otherLength = 0;
        long otherLastModified = 0;
        if (otherURL.getProtocol().startsWith("http")) {
            HttpURLConnection urlConnection = null;
            try {
                urlConnection = (HttpURLConnection)otherURL.openConnection();
                urlConnection.setRequestMethod("HEAD");
                otherLength = urlConnection.getContentLength();
                otherLastModified = urlConnection.getLastModified();
            } catch (IOException e) {
                if (LOG.isLoggable(Level.WARNING)) LOG.log(Level.WARNING, "Failed to obtain age and otherLength of URL " + otherURL, e);
            } finally {
                try {
                    if (urlConnection != null) {
                        final InputStream inputStream = urlConnection.getInputStream();
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException ignore) {
                                // ignore
                            }
                        }
                    }
                } catch (IOException ignore) {
                    // ignore
                }
            }
        }
        else {
            URLConnection urlConnection = null;
            try {
                urlConnection = otherURL.openConnection();
                otherLength = urlConnection.getContentLength();
                otherLastModified = urlConnection.getLastModified();
            } catch (IOException e) {
                if (LOG.isLoggable(Level.WARNING)) LOG.log(Level.WARNING, "Failed to obtain age and otherLength of URL " + otherURL, e);
            } finally {
                try {
                    if (urlConnection != null) {
                        final InputStream inputStream = urlConnection.getInputStream();
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException ignore) {
                                // ignore
                            }
                        }
                    }
                } catch (IOException ignore) {
                    // ignore
                }
            }
        }

        return this.lastModified != otherLastModified || this.length != otherLength;
    }

    public Iterator getGCEvents() {
        return events.iterator();
    }

    public Iterator getConcurrentGCEvents() {
        return concurrentGCEvents.iterator();
    }

    public Iterator getEvents() {
        return allEvents.iterator();
    }

    public void add(final AbstractGCEvent abstractEvent) {
        allEvents.add(abstractEvent);
        if (abstractEvent instanceof ConcurrentGCEvent) {
            concurrentGCEvents.add(abstractEvent);
        } else if (abstractEvent instanceof GCEvent) {
            events.add(abstractEvent);
            final GCEvent event = (GCEvent) abstractEvent;
            footprint = Math.max(footprint, event.getTotal());
            runningTime = Math.max(runningTime, event.getTimestamp());
            freedMemory += event.getPreUsed() - event.getPostUsed();
            pause.add(event.getPause());
            if (event.getType().getGeneration() != GCEvent.Generation.TENURED && !event.hasTenuredDetail()) {
                gcEvents.add(event);
                postGCUsedMemory.add(event.getPostUsed());
                freedMemoryByGC.add(event.getPreUsed() - event.getPostUsed());
                currentNoFullGCEvents.add(event);
                currentPostGCSlope.addPoint(event.getTimestamp(), event.getPostUsed());
                currentRelativePostGCIncrease.addPoint(currentRelativePostGCIncrease.getPointCount(), event.getPostUsed());
                gcPause.add(event.getPause());
            } else {
                if (event.getType().getGeneration() == GCEvent.Generation.TENURED || event.hasTenuredDetail()) {
                    fullGCEvents.add(event);
                    postFullGCUsedMemory.add(event.getPostUsed());
                    final int freed = event.getPreUsed() - event.getPostUsed();
                    freedMemoryByFullGC.add(freed);
                    fullGCPause.add(event.getPause());
                    postFullGCSlope.addPoint(event.getTimestamp(), event.getPostUsed());
                    relativePostFullGCIncrease.addPoint(relativePostFullGCIncrease.getPointCount(), event.getPostUsed());
                }
                // process no full-gc run data
                if (currentPostGCSlope.hasPoints()) {
                    // make sure we have at least _two_ data points
                    if (currentPostGCSlope.isLine()) {
                        postGCSlope.add(currentPostGCSlope.slope(), currentPostGCSlope.getPointCount());
                        relativePostGCIncrease.add(currentRelativePostGCIncrease.slope(), currentRelativePostGCIncrease.getPointCount());
                    }
                    currentPostGCSlope.reset();
                    currentRelativePostGCIncrease.reset();
                }
            }
        }
    }

    public int size() {
        return events.size();
    }

    public AbstractGCEvent get(final int index) {
        return (AbstractGCEvent) events.get(index);
    }


    /**
     * Pauses caused by full garbage collections.
     */
    public DoubleData getFullGCPause() {
        return fullGCPause;
    }

    /**
     * Pauses caused by garbage collections.
     */
    public DoubleData getGCPause() {
        return gcPause;
    }

    /**
     * The increase in memory consumption after a full collection in relation to the amount that was
     * used after the previous full collection.
     */
    public RegressionLine getRelativePostFullGCIncrease() {
        return relativePostFullGCIncrease;
    }

    /**
     * The increase in memory consumption after a collection in relation to the amount that was
     * used after the previous collection.
     */
    public DoubleData getRelativePostGCIncrease() {
        return relativePostGCIncrease;
    }

    /**
     * The average slope of the regression lines of the memory consumption after
     * a garbage collection in between <em>full</em> garbage collections.
     * <p/>
     * The unit is kb/s.
     */
    public double getPostGCSlope() {
        return postGCSlope.average();
    }

    public IntData getPostGCUsedMemory() {
        return postGCUsedMemory;
    }

    public RegressionLine getCurrentPostGCSlope() {
        return currentPostGCSlope;
    }

    public RegressionLine getPostFullGCSlope() {
        return postFullGCSlope;
    }

    public IntData getPostFullGCUsedMemory() {
        return postFullGCUsedMemory;
    }

    /**
     * Heap memory freed by a (small) garbage collection.
     */
    public IntData getFreedMemoryByGC() {
        return freedMemoryByGC;
    }

    /**
     * Heap memory freed by a <em>full</em> garbage collection.
     */
    public IntData getFreedMemoryByFullGC() {
        return freedMemoryByFullGC;
    }

    /**
     * Heap memory consumption after a (small) garbage collection.
     */
    public IntData getFootprintAfterGC() {
        return postGCUsedMemory;
    }

    /**
     * Heap memory consumption after a <em>full</em> garbage collection.
     */
    public IntData getFootprintAfterFullGC() {
        return postFullGCUsedMemory;
    }

    /**
     * Pause in sec.
     */
    public DoubleData getPause() {
        return pause;
    }

    /**
     * Throughput in percent.
     */
    public double getThroughput() {
        return 100 * (runningTime - pause.getSum()) / runningTime;
    }

    /**
     * Footprint in KB.
     */
    public long getFootprint() {
        return footprint;
    }

    /**
     * Running time in sec.
     */
    public double getRunningTime() {
        return runningTime;
    }

    /**
     * Freed memory in KB.
     */
    public long getFreedMemory() {
        return freedMemory;
    }

    public Format getFormat() {
        return format;
    }

    public void setFormat(final Format format) {
        this.format = format;
    }

    public boolean hasCorrectTimestamp() {
        return format == Format.IBM_VERBOSE_GC || format == Format.SUN_X_LOG_GC || format == Format.SUN_1_2_2VERBOSE_GC;
    }

    public String toString() {
        return events.toString();
    }

    public static class Format implements Serializable {
        private String format;

        private Format(final String format) {
            this.format = format;
        }

        public String toString() {
            return format;
        }

        public static final Format SUN_VERBOSE_GC = new Format("Sun -verbose:gc");
        public static final Format SUN_X_LOG_GC = new Format("Sun -Xloggc:<file>");
        public static final Format IBM_VERBOSE_GC = new Format("IBM -verbose:gc");
        public static final Format SUN_1_2_2VERBOSE_GC = new Format("Sun 1.2.2 -verbose:gc");
    }
}
