package org.limewire.ui.swing.properties;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.GridBagLayout;
import java.awt.Paint;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import java.text.NumberFormat;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

import net.miginfocom.swing.MigLayout;

import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.painter.RectanglePainter;
import org.limewire.bittorrent.Torrent;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.download.DownloadItem;
import org.limewire.core.api.download.DownloadPiecesInfo;
import org.limewire.core.api.download.DownloadState;
import org.limewire.core.api.download.DownloadItem.DownloadItemType;
import org.limewire.core.api.download.DownloadPiecesInfo.PieceState;
import org.limewire.core.api.library.PropertiableFile;
import org.limewire.ui.swing.util.FontUtils;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.PainterUtils;
import org.limewire.ui.swing.util.ResizeUtils;

public class FileInfoPiecesPanel implements FileInfoPanel {

    private static final int NUM_COLUMNS = 10;
    private static final int MAX_NUM_ROWS = 10;
    private static final int MAX_CELL_HEIGHT = 16;
    private static final int MAX_CELL_WIDTH = 80;

    private static final PieceIntensity ACTIVE_INTENSITY = new PieceIntensity(PieceState.ACTIVE);
    private static final PieceIntensity UNAVAILABLE_INTENSITY = new PieceIntensity(PieceState.UNAVAILABLE);
    
    private final NumberFormat formatter = new DecimalFormat("0.00");
    
    /**
     * The shorter the cooler, the shorter the more performance intensive.
     */
    private static final int REFRESH_DELAY = 200;
    
    @Resource private Color foreground;
    @Resource private Font smallFont;
    @Resource private Color legendBackground = PainterUtils.TRANSPARENT;
    
    @Resource private Color downloadedForeground;
    @Resource private Color availableForeground;
    @Resource private Color activeForeground;
    @Resource private Color unavailableForeground;
    
    private final Color partialForegroundInitial;
    private final Color partialForegroundFinal;
    
    private final DownloadItem download;
    private Torrent torrent = null;
    
    private final JPanel component;
 
    private PiecesGrid grid;
    private Timer refresher;
    
    private DownloadPiecesInfo piecesInfo;
    private int numPieces = -1;
    private int coalesceFactor;
    
    private boolean finishedSuccessfully = false;
    private int cachedColumns;
    private int cachedRows;
    
    private final JLabel statusLabel;
    private JLabel numPiecesLabel;
    private JLabel piecesPerCellLabel;
    private JLabel piecesSizeLabel;
    private JLabel ratioLabel;
    private JLabel uploadedLabel;
    private JLabel downloadedLabel;
    private JLabel piecesCompletedLabel;
    private JLabel failedDownloadLabel;
    
    public FileInfoPiecesPanel(final DownloadItem download) {
        this.download = download;
        
        GuiUtils.assignResources(this);
        
        partialForegroundInitial = createShade(availableForeground, downloadedForeground, .1);
        partialForegroundFinal = createShade(downloadedForeground, availableForeground, .1);
        
        component = new JPanel(new MigLayout("insets 6 0 0 0"));
        component.setOpaque(false);
                
        numPiecesLabel = createLabel("?");
        piecesPerCellLabel = createLabel("?");
        piecesCompletedLabel = createLabel("?");
        piecesSizeLabel = createLabel("?");
        downloadedLabel = createLabel("?");
        failedDownloadLabel = createLabel("?");
                
        grid = new PiecesGrid();
        
        statusLabel = new JLabel(I18n.tr("Preparing View..."));
        grid.setLayout(new GridBagLayout());
        grid.add(statusLabel);
                
        ResizeUtils.forceSize(grid, new Dimension(MAX_CELL_WIDTH*NUM_COLUMNS, 
                MAX_CELL_HEIGHT*MAX_NUM_ROWS));

        final JPanel infoPanel = new JPanel(new MigLayout("insets 0, gap 0, fill"));
        infoPanel.setOpaque(false);
        
        JPanel legendPanel = new JPanel(new MigLayout("insets 8, gap 3"));
        legendPanel.setBackground(legendBackground);
        
        legendPanel.add(createLegendBox(activeForeground));
        legendPanel.add(createLabel(I18n.tr("Active")), "gapright 5");
        legendPanel.add(createLegendBox(availableForeground));
        legendPanel.add(createLabel(I18n.tr("Available")), "wrap");
        legendPanel.add(createLegendBox(downloadedForeground));
        legendPanel.add(createLabel(I18n.tr("Done")), "gapright 5");
        legendPanel.add(createLegendBox(unavailableForeground));
        legendPanel.add(createLabel(I18n.tr("Unavailable")), "wrap");
        legendPanel.add(createLegendBox(new GradientPaint(0, 0, partialForegroundInitial, 0, 1, partialForegroundFinal)));
        legendPanel.add(createLabel(I18n.tr("Partially Done")), "wrap");
        
        JPanel bottomLegend = new JPanel(new MigLayout("insets 0, gap 6, fillx"));
        bottomLegend.setOpaque(false);
        bottomLegend.add(createLabel(I18n.tr("Pieces per Cell:")));
        bottomLegend.add(piecesPerCellLabel);
        legendPanel.add(bottomLegend, "gaptop 5, span, alignx 50%");
        
        JPanel rightPanel = new JPanel(new MigLayout("fillx, gap 0, insets 0"));
        rightPanel.setOpaque(false);
        rightPanel.add(legendPanel);
        infoPanel.add(rightPanel, "dock east");
        
        infoPanel.add(createBoldLabel(I18n.tr("Number of Pieces:")), "split 2");
        infoPanel.add(numPiecesLabel, "wrap");
                
        infoPanel.add(createBoldLabel(I18n.tr("Pieces Completed:")), "split 2");
        infoPanel.add(piecesCompletedLabel, "wrap");
        
        infoPanel.add(createBoldLabel(I18n.tr("Piece Size:")), "split 2");
        infoPanel.add(piecesSizeLabel, "wrap");
        
        infoPanel.add(createBoldLabel(I18n.tr("Downloaded:")), "split 2");
        infoPanel.add(downloadedLabel, "wrap");
        
        infoPanel.add(createBoldLabel(I18n.tr("Failed Download:")), "split 2");
        infoPanel.add(failedDownloadLabel, "wrap");
        
        if (download.getDownloadItemType() == DownloadItemType.BITTORRENT) {
            torrent = (Torrent) download.getProperty(FilePropertyKey.TORRENT);
        
            uploadedLabel = createLabel("");
            ratioLabel = createLabel("");
            
            infoPanel.add(createBoldLabel(I18n.tr("Uploaded:")), "split 2");
            infoPanel.add(uploadedLabel, "wrap");
            
            infoPanel.add(createBoldLabel(I18n.tr("Ratio:")), "split 2");
            infoPanel.add(ratioLabel, "wrap");
        }
                
        component.add(grid, "wrap");
        component.add(infoPanel, "growx");
        
        refresher = new Timer(REFRESH_DELAY, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {

                if (isFinished()) {
                    refresher.stop();
                    
                    if (download.getState() == DownloadState.CANCELLED) {
                        grid.setAlpha(.5f);
                        statusLabel.setText(I18n.tr("Download cancelled"));
                        statusLabel.setVisible(true);
                        grid.repaint();
                    } else {
                        finishedSuccessfully = true;
                        if (numPieces > 0) {
                            for ( int i=0 ; i<grid.getCellCount() ; i++ ) {
                                grid.setCellFillPaint(i, downloadedForeground);
                            }
                        } else {
                            statusLabel.setText(I18n.tr("Download already finished"));
                        }
                    }
                } 
                else {
                    if (calculatePieceData() > 0) {
                        updateTable();
                    }
                }
                
                grid.repaint();
                
                // Hack to get the legends margin to line up with the table edge despite
                //  resizing due to rounding.  NOTE: there will be a lag of one refresh cycle. 
                if (grid.isMarginUpdated()) {
                    int margin = grid.getInnerMargin();
                    if (margin != 0) {
                        infoPanel.setBorder(BorderFactory.createEmptyBorder(0, margin, 0, margin));
                    }
                }
                
                updateDownloadDetails();
            }

            private boolean isFinished() {
                DownloadState state = download.getState();
                
                // TODO: use DownloadState.isFinished() ?  doesn't match properly right now though.
                
                return state == DownloadState.DONE || state == DownloadState.FINISHING
                         || state == DownloadState.CANCELLED || state == DownloadState.DANGEROUS;
            }
        });
        
        refresher.setInitialDelay(0);
        refresher.start();
    }
    
    private int calculatePieceData() {
        piecesInfo = download.getPiecesInfo();
        
        if (piecesInfo == null) {
            return -1;
        }
        
        int newNum = piecesInfo.getNumPieces();
        if(newNum != numPieces && newNum != 0) {
            // Hide any previous status messages
            statusLabel.setVisible(false);
            
            // If the number of pieces has changed, resize the grid.
            numPieces = newNum;
            setupGrid();
        }
        return newNum;
    }
    
    private void setupGrid() {        
        int requiredRows = (int)Math.ceil((double)numPieces / NUM_COLUMNS);
        int numRows = requiredRows;
        
        coalesceFactor = 1;
        if (requiredRows > MAX_NUM_ROWS) {
            coalesceFactor = (int)Math.ceil((double)numPieces / (MAX_NUM_ROWS*NUM_COLUMNS));
            numRows = (int)Math.ceil((double)numPieces / (coalesceFactor*NUM_COLUMNS));
            piecesPerCellLabel.setText(""+coalesceFactor);
        }
        
        grid.resizeGrid(numRows, NUM_COLUMNS);
        grid.setAlignmentX(Component.CENTER_ALIGNMENT);
        grid.setAlignmentY(Component.CENTER_ALIGNMENT);
        ResizeUtils.forceSize(grid, new Dimension(MAX_CELL_WIDTH*NUM_COLUMNS, 
                                                  MAX_CELL_HEIGHT*numRows));
    }
    
    private static Component createLegendBox(Paint foreground) {
        JXPanel panel = new JXPanel();
        RectanglePainter<JXPanel> painter = new RectanglePainter<JXPanel>();
        painter.setPaintStretched(true);
        painter.setFillPaint(foreground);
        painter.setBorderPaint(Color.BLACK);
        painter.setAntialiasing(true);
        painter.setCacheable(true);
        panel.setBackgroundPainter(painter);
        return panel;
    }
    
    private JLabel createLabel(String text) {
        JLabel label = new JLabel(text);
        label.setOpaque(false);
        label.setForeground(foreground);
        label.setFont(smallFont);
        return label;
    }
    
    private JLabel createBoldLabel(String text) {
        JLabel label = createLabel(text);
        FontUtils.bold(label);
        return label;
    }
    
    private void updateDownloadDetails() {
        
        long currentSize = download.getCurrentSize();
        long verifiedSize = download.getAmountVerified();
        
        String downloadString = GuiUtils.formatUnitFromBytes(currentSize);
        
        // Don't show verification unless the numbers deviate more than one byte
        if (currentSize - verifiedSize > 1000) {
            downloadString += " " + I18n.tr("({0} verified)", GuiUtils.formatUnitFromBytes(verifiedSize));
        }
        
        downloadedLabel.setText(downloadString);
        
        if (numPieces > 0) {
            numPiecesLabel.setText("" + numPieces);
        } 
        
        if (piecesInfo != null) {
            int completed = piecesInfo.getNumPiecesCompleted();
        
            String completedText;
            if (finishedSuccessfully) {
                completedText = "" + numPieces;  
            } 
            else if (completed < 0) {
                completedText = "?";
            } 
            else {
                completedText = "" + completed;
            }
            
            piecesCompletedLabel.setText(completedText);
            piecesSizeLabel.setText(GuiUtils.formatUnitFromBytes(piecesInfo.getPieceSize()));
        }
                
        failedDownloadLabel.setText(GuiUtils.formatUnitFromBytes(download.getAmountLost()));
        
        if (torrent != null) {
            uploadedLabel.setText(GuiUtils.formatUnitFromBytes(torrent.getTotalUploaded()));
            ratioLabel.setText(formatter.format(torrent.getSeedRatio()));
        }
    }
    
    private void updateTable() {
 
        // Correction if per chance the number of cells or rows is changed.
        if (cachedRows != grid.getRows() || cachedColumns != grid.getColumns()) {
            cachedRows = grid.getRows();
            cachedColumns = grid.getColumns();
            
            int cellsAvailable = grid.getCellCount();
            coalesceFactor = (int)Math.ceil((double)numPieces / cellsAvailable);
            piecesPerCellLabel.setText(""+coalesceFactor);
        }
        
        int gridSlot = 0;
        
        for ( int i=0 ; i<numPieces ; i+=coalesceFactor ) {
            Paint pieceForeground = Color.BLACK;
        
            int numPiecesLeft = numPieces-coalesceFactor*gridSlot;
            int piecesToCoalesce = coalesceFactor;
            if (numPiecesLeft < coalesceFactor) {
                piecesToCoalesce = numPiecesLeft;
            }
            
            PieceIntensity cumulativePieceIntensity = coalescePieceStates(piecesInfo, i, piecesToCoalesce); 
            PieceState cumulativeState = cumulativePieceIntensity.getState();
            
            switch (cumulativeState) {
                case ACTIVE :
                    pieceForeground = activeForeground;
                    break;
                case PARTIAL :
                    pieceForeground = createShade(partialForegroundInitial, partialForegroundFinal,
                            cumulativePieceIntensity.getIntensity());
                    break;
                case AVAILABLE :
                    pieceForeground = availableForeground;
                    break;
                case DOWNLOADED :
                    pieceForeground = downloadedForeground;
                    break;
                case UNAVAILABLE :
                    pieceForeground = unavailableForeground;
                    break;
                default:
                    throw new IllegalStateException(cumulativeState.toString());
            }
            
            grid.setCellFillPaint(gridSlot++, pieceForeground);
        }
    }
    
    @Override
    public JComponent getComponent() {
        return component;
    }

    @Override
    public boolean hasChanged() {
        return false;
    }

    @Override
    public void save() {
    }

    @Override
    public void updatePropertiableFile(PropertiableFile file) {
    }

    @Override
    public void dispose() {
        refresher.stop();
    }
    
    private static PieceIntensity coalescePieceStates(DownloadPiecesInfo piecesInfo, int startIndex, int piecesToCoalesce) {
        
        // +1 for partial, +2 for done, 0 for available
        int completedScore = 0;
                
        PieceState workingState = null;
        
        for ( int i=startIndex ; i < startIndex+piecesToCoalesce ; i++ ) {
            PieceState state = piecesInfo.getPieceState(i);
            if (state == PieceState.ACTIVE) {
                return ACTIVE_INTENSITY;
            }
            if (state == PieceState.UNAVAILABLE) {
                return UNAVAILABLE_INTENSITY;
            }
            
            switch (state) {
                case PARTIAL :
                    completedScore+=1;
                    break;
                case DOWNLOADED :
                    completedScore+=2;
                    break;
            }
            
            if (workingState != null && workingState != state) {
                workingState = PieceState.PARTIAL;                   
            } 
            else {            
                workingState = state;
            }
        }
        
        if (workingState == PieceState.PARTIAL) {
            int completedScoreMax = piecesToCoalesce*2;
            return new PieceIntensity(workingState, (double)completedScore/completedScoreMax);
        } else {
            return new PieceIntensity(workingState);
        }
        
    }
    
    private static class PieceIntensity {
        private final PieceState state;
        private final double intensity;
        
        public PieceIntensity(PieceState state) {
            this.state = state;
            this.intensity = 1;
        }
        
        public PieceIntensity(PieceState state, double intensity) {
            this.state = state;
            this.intensity = intensity;
        }
        
        public PieceState getState() {
            return state;
        }
        
        public double getIntensity() {
            return intensity;
        }
    }   
    
    private static Color createShade(Color initialShade, Color finalShade, double intensity) {
        int redDelta = finalShade.getRed() - initialShade.getRed();
        int greenDelta = finalShade.getGreen() - initialShade.getGreen();
        int blueDelta = finalShade.getBlue() - initialShade.getBlue();
        
        redDelta *= intensity;
        greenDelta *= intensity;
        blueDelta *= intensity;
        
        return new Color(initialShade.getRed() + redDelta,
                initialShade.getGreen() + greenDelta,
                initialShade.getBlue() + blueDelta);
    }
}