/*
 * TV-Browser
 * Copyright (C) 04-2003 Martin Oberhauser (martin_oat@yahoo.de)
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * CVS information:
 *  $RCSfile$
 *   $Source$
 *     $Date: 2015-12-19 23:30:01 +0100 (Sa, 19 Dez 2015) $
 *   $Author: ds10 $
 * $Revision: 8461 $
 */
package util.ui;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import tvbrowser.core.Settings;
import tvbrowser.core.icontheme.InfoIconTheme;
import tvbrowser.core.icontheme.InfoThemeLoader;
import tvbrowser.core.plugin.PluginProxy;
import tvbrowser.core.plugin.PluginProxyManager;
import tvbrowser.core.plugin.PluginStateListener;
import tvbrowser.extras.common.InternalPluginProxyIf;
import tvbrowser.extras.common.InternalPluginProxyList;
import tvbrowser.extras.favoritesplugin.FavoritesPluginProxy;
import tvbrowser.extras.favoritesplugin.core.Favorite;
import tvbrowser.extras.favoritesplugin.dlgs.FavoriteTreeModel;
import util.io.IOUtilities;
import util.misc.StringPool;
import util.program.ProgramUtilities;
import util.programmouseevent.ProgramMouseAndContextMenuListener;
import util.programmouseevent.ProgramMouseEventHandler;
import util.settings.ProgramPanelSettings;
import devplugin.ContextMenuIf;
import devplugin.Marker;
import devplugin.Plugin;
import devplugin.PluginAccess;
import devplugin.Program;
import devplugin.ProgramFieldType;
import devplugin.ProgramInfoHelper;
import devplugin.ToolTipIcon;

/**
 * A ProgramPanel is a JComponent representing a single program.
 *
 * @author Martin Oberhauser
 * @author Til Schneider, www.murfman.de
 */
public class ProgramPanel extends JComponent implements ChangeListener, PluginStateListener {

  private static final Logger mLog = java.util.logging.Logger
      .getLogger(TextAreaIcon.class.getName());

  private static final util.ui.Localizer mLocalizer = util.ui.Localizer
      .getLocalizerFor(ProgramPanel.class);

  private static final boolean USE_FULL_HEIGHT = true;

  private static final Composite NORMAL_COMPOSITE = AlphaComposite.SrcOver;
  private static final Composite PALE_COMPOSITE = AlphaComposite.getInstance(
      AlphaComposite.SRC_OVER, 0.5F);

  private int mMarkTime;
  
  /** The title font. */
  private static Font mTitleFont;
  /** The time font. */
  private static Font mTimeFont;
  /** The normal font */
  private static Font mNormalFont;
  /**
   * the font size offset for the currently selected dynamic font size
   */
  private static int fontSizeOffset = 0;

  private static int columnWidthOffset = 0;

  /** The width of the channel logo */
 // private static int WIDTH_LOGO = 0;
  /** The width of the left part (the time). */
  public static int WIDTH_LEFT = -1;
  /** The width of the right part (the title and short info). */
  private static int WIDTH_RIGHT = Settings.propColumnWidth.getInt() - WIDTH_LEFT/* - WIDTH_LOGO*/;
  /** The total width. */
  private static int WIDTH_TOTAL =/* WIDTH_LOGO +*/ WIDTH_LEFT + WIDTH_RIGHT;
  
  /** Formatter for the Time-String */
  private static final TimeFormatter TIME_FORMATTER = new TimeFormatter();

  /**
   * horizontal distance between format icons
   */
  private static final int ICON_DISTANCE_X = 2;

  /**
   * vertical distance between format icons
   */
  private static final int ICON_DISTANCE_Y = 2;

  /** The height. */
  private int mHeight = 0;
  /**
   * The preferred height.
   * <p>
   * It's the height the panel has with a maximum of 3 information rows.
   */
  private int mPreferredHeight = 0;
  
  private ChannelLabel mChannelLabel;
  /** The start time as String. */
  private String mProgramTimeAsString;
  /** The icon used to render the title. */
  private TextAreaIcon mTitleIcon;
  /** The icon used to render the description. */
  private TextAreaIcon mDescriptionIcon;

  /** The icon used to paint the picture with */
  private PictureAreaIcon mPictureAreaIcon;

  /** The icons to show on the left side under the start time. */
  private Icon[] mIconArr;
  /** The program. */
  private Program mProgram;

  /** Color of the Text */
  private Color mTextColor = (!Settings.propTableBackgroundStyle.getString().equals("uiColor") && !Settings.propTableBackgroundStyle.getString().equals("uiTimeBlock")) ? Settings.propProgramPanelForegroundColor.getColor() : UIManager.getColor("List.foreground");

  /** Panel under a Mouse ? */
  private boolean mMouseOver = false;

  private boolean mIsSelected = false;

  /**
   * this panel has been changed by third party, needs update before painting
   */
  private boolean mHasChanged = false;

  /** Orientation of progress bar */
  private int mAxis = ProgramPanelSettings.Y_AXIS;

  /** The vertical gap between the programs */
  private static int V_GAP = 5;

  private ProgramPanelSettings mSettings;

  private boolean mPaintExpiredProgramsPale = true;

  /**
   * rectangle of the format icon area, used for tooltip calculation
   */
  private Rectangle mInfoIconRect;

  /** The importance of this program */
  private byte mProgramImportance;
  
  private int mLogoWidth = 0;

  /**
   * Creates a new instance of ProgramPanel.
   */
  public ProgramPanel() {
    this(new ProgramPanelSettings(Settings.propPictureType.getInt(), Settings.propPictureStartTime.getInt(), Settings.propPictureEndTime.getInt(), false, Settings.propIsPictureShowingDescription.getBoolean(), Settings.propPictureDuration.getInt(), Settings.propPicturePluginIds.getStringArray(), ProgramPanelSettings.Y_AXIS, false, Settings.propShowProgramTablePictureBorder.getBoolean(), false));
  }
  
  /**
   * Creates a new instance of ProgramPanel.
   * @param settings The settings for this program panel.
   *
   * @since 2.2.2
   */
  public ProgramPanel(ProgramPanelSettings settings) {
    setToolTipText("");
    mSettings = settings;
    mAxis = settings.getAxis();
    mMarkTime = -1;

    if (mTitleFont == null) {
      updateFonts();
    }
    
    calculateWidth();
    
    int minWidth = Math.max(WIDTH_RIGHT - 5 - mLogoWidth, 55-WIDTH_LEFT);
    
    mTitleIcon = new TextAreaIcon(null, mTitleFont, minWidth);
    mDescriptionIcon = new TextAreaIcon(null, mNormalFont, minWidth);
    mDescriptionIcon.setMaximumLineCount(3);

    setBackground(UIManager.getColor("programPanel.background"));
  }

  /**
   * Creates a new instance of ProgramPanel.
   *
   * @param prog
   *          The program to show in this panel.
   */
  public ProgramPanel(Program prog) {
    this();
    setProgram(prog);
  }

  /**
   * Creates a new instance of ProgramPanel.
   *
   * @param prog
   *          The program to show in this panel.
   * @param settings The settings for this program panel.
   *
   * @since 2.2.2
   */
  public ProgramPanel(Program prog, ProgramPanelSettings settings) {
    this(settings);
    setProgram(prog);
  }

  /**
   * Creates a new instance of ProgramPanel.
   *
   * @param prog
   *          The program to show in this panel.
   * @param axis
   *          Orientation of ProgressBar (X_AXIS/Y_AXIS)
   * @param settings The settings for this program panel.
   *
   * @since 2.2.2
   */
  public ProgramPanel(Program prog, int axis, ProgramPanelSettings settings) {
    this(settings);
    mAxis = axis;
    setProgram(prog);
  }
  
  private void calculateWidth() {
	if(mSettings.isShowingChannelLogo()) {
		mLogoWidth = 46;
	}
	  
    if (WIDTH_LEFT == -1) {
      // distance between time and title shall match title font settings, but at
      // most n pixels
      int distance = getFontMetrics(mTitleFont).stringWidth("n");
      if (distance > 7) {
        distance = 7;
      }
      
      WIDTH_LEFT = getFontMetrics(mTimeFont).stringWidth(
          TIME_FORMATTER.formatTime(23, 59))
          + distance;
      WIDTH_RIGHT = Settings.propColumnWidth.getInt() + columnWidthOffset
          - WIDTH_LEFT;
      
      if(WIDTH_RIGHT-5 < 50) {
        WIDTH_RIGHT = 50;
      }
      
      WIDTH_TOTAL = WIDTH_LEFT + WIDTH_RIGHT;
    }
  }

  /**
   * Reloads the font settings and sets a dynamic font size
   * @param newOffset increase or decrease compared to standard font size
   * @since 2.6
   */
  public static boolean updateFonts(int newOffset) {
    Font oldTitleFont = mTitleFont;
    Font oldTimeFont = mTimeFont;
    Font oldNormalFont = mNormalFont;
    boolean useDefaults = Settings.propUseDefaultFonts.getBoolean();
    if (useDefaults) {
      mTitleFont = Settings.propProgramTitleFont.getDefault();
      mTimeFont = Settings.propProgramTimeFont.getDefault();
      mNormalFont = Settings.propProgramInfoFont.getDefault();
    } else {
      mTitleFont = Settings.propProgramTitleFont.getFont();
      mTimeFont = Settings.propProgramTimeFont.getFont();
      mNormalFont = Settings.propProgramInfoFont.getFont();
    }
    if (newOffset == 0) {
      fontSizeOffset = 0;
    }
    else {
      fontSizeOffset += newOffset;
    }
    if (fontSizeOffset != 0) {
      mTitleFont = getDynamicFontSize(mTitleFont, fontSizeOffset);
      mTimeFont = getDynamicFontSize(mTimeFont, fontSizeOffset);
      mNormalFont = getDynamicFontSize(mNormalFont, fontSizeOffset);
    }
    // if the font didn't change, avoid updates
    if (mTitleFont.equals(oldTitleFont) && mTimeFont.equals(oldTimeFont) && mNormalFont.equals(oldNormalFont)) {
      return false;
    }
    // reset width of the left part, will be recalculated during first forceRepaint
    WIDTH_LEFT = -1;
    return true;
  }

private static Font getDynamicFontSize(Font font, int offset) {
  float size = font.getSize2D() + offset;
  if (size < 4) {
    size = 4;
  }
  return font.deriveFont(size);
}

  /**
   * Reloads the font settings and sets the dynamic font size
   * to standard font size again
   */
  public static void updateFonts() {
    updateFonts(0);
  }

  /**
   * Change the settings of this panel.
   *
   * @param settings The settings of this panel.
   */
  public void setProgramPanelSettings(ProgramPanelSettings settings) {
    mSettings = settings;
  }

  /**
   * Repaints the complete panel.
   */
  public void forceRepaint() {
    calculateWidth();
    mTitleIcon = new TextAreaIcon(null, mTitleFont, WIDTH_RIGHT - 5);
    mDescriptionIcon = new TextAreaIcon(null, mNormalFont, WIDTH_RIGHT - 5);
    mDescriptionIcon.setMaximumLineCount(Settings.propProgramPanelMaxLines
        .getInt());
    mProgram.validateMarking();
    Program p = mProgram;
    mProgram = null;
    setProgram(p);
  }

  /**
   * (Re)Loads the column width settings.
   */
  public static void updateColumnWidth() {
    updateColumnWidth(0);
  }

  public static int updateColumnWidth(int newOffset) {
    if (newOffset == 0) {
      columnWidthOffset = 0;
    }
    else {
      columnWidthOffset += (20 * newOffset);
    }
    int columnWidth = Settings.propColumnWidth.getInt() + columnWidthOffset;
    if (columnWidth < Settings.MIN_COLUMN_WIDTH) {
      columnWidthOffset = Settings.MIN_COLUMN_WIDTH - Settings.propColumnWidth.getInt();
    }
    if (columnWidth > Settings.MAX_COLUMN_WIDTH) {
      columnWidthOffset = Settings.MAX_COLUMN_WIDTH - Settings.propColumnWidth.getInt();
    }
    columnWidth = Settings.propColumnWidth.getInt() + columnWidthOffset;
    WIDTH_RIGHT = columnWidth - WIDTH_LEFT;
    WIDTH_TOTAL = WIDTH_LEFT + WIDTH_RIGHT;
    return columnWidth;
  }

  /**
   * Gets the preferred height.
   * <p>
   * It's the height the panel has with a maximum of 3 information rows.
   *
   * @return The preferred height.
   */
  public int getPreferredHeight() {
    return mPreferredHeight;
  }

  /**
   * Sets the height of this panel
   *
   * @param height
   */
  public void setHeight(int height) {
    if (mHeight != height) {
      setProgram(getProgram(), height);
      mHeight = height;
    }
  }

  public void setMaximumHeight() {
    setProgram(getProgram(), 10000);
  }

  /**
   * Gets the height.
   *
   * @return The height.
   */
  public int getHeight() {
    return mHeight;
  }

  /**
   * Sets the program this panel shows.
   *
   * @param program
   *          The program to show in this panel.
   */
  public void setProgram(Program program) {
    setProgram(program, -1);
  }
  
  private boolean dontShowPictureAreaIcon(boolean dontShow) {
    if(mSettings.isShowingPictureForPlugins()) {
      String[] pluginIds = mSettings.getPluginIds();
      Marker[] markers = mProgram.getMarkerArr();

      if(markers != null && pluginIds != null) {
        for (Marker marker : markers) {
          for (String pluginId : pluginIds) {
            if(marker.getId().compareTo(pluginId) == 0) {
              dontShow = false;
              break;
            }
          }
        }
      }
    }
    
    return dontShow;
  }
  
  /**
   * @return If the picture state is changed.
   */
  public boolean pictureStateChanged() { 
    boolean show = showPicture(mProgram, dontShowPictureAreaIcon(true));
    
    if(!show && mPictureAreaIcon.getIconHeight() > 0 || show && mPictureAreaIcon.getIconHeight() == 0) {
      return mProgram.hasFieldValue(ProgramFieldType.PICTURE_TYPE);
    }
    
    return false;
  }

  /**
   * Sets the program this panel shows.
   *
   * @param program
   *          The program to show in this panel.
   * @param maxHeight
   *          The maximum height the program should have (in pixels).
   */
  public void setProgram(Program program, int maxHeight) {
    Program oldProgram = mProgram;
    mProgram = program;

    if (Settings.propProgramTableCutTitle.getBoolean()) {
      mTitleIcon.setMaximumLineCount(Settings.propProgramTableCutTitleLines
          .getInt());
    } else {
      mTitleIcon.setMaximumLineCount(-1);
    }
    mDescriptionIcon.setMaximumLineCount(-1);
    
    if(mSettings.isShowingChannelLogo()) {
	  try {
	    mChannelLabel = new ChannelLabel(true,false,false,false,false,false,false);
	    mChannelLabel.setChannel(program.getChannel());
	  }catch(Exception e) {
	    mLog.log(Level.SEVERE, "Could not load channelLabel for program '" + program + "'", e);
	  }
    }

    boolean programChanged = oldProgram == null || !oldProgram.equals(program);
    
    if (programChanged) {
      // Get the start time, filter duplicate strings
      mProgramTimeAsString = StringPool.getString(program.getTimeString());
      
      // Set the new title
      mTitleIcon.setText(program.getTitle());

      if(mProgram.getProgramState() == Program.IS_VALID_STATE) {
        programHasChanged();
      }
    }
    
    int length = program.getLength();
    // Create the picture area icon
    if(showPicture(program,dontShowPictureAreaIcon(true))) {
      int pictureWidth = Math.max(WIDTH_RIGHT - 4 - mLogoWidth, 56-WIDTH_LEFT);
      
      if(textIconWidth - 156 > 0) {
        pictureWidth = Math.min(156, textIconWidth);
      }
      
      mPictureAreaIcon = new PictureAreaIcon(program,mNormalFont, pictureWidth, mSettings.isShowingPictureDescription(), true, false, mSettings.isShowingPictureBorder());
    } else {
      mPictureAreaIcon = new PictureAreaIcon();
    }

    // Calculate the maximum description lines
    int titleHeight = mTitleIcon.getIconHeight();
    int maxDescLines;
    if (Settings.propProgramPanelShortDurationActive.getBoolean()
        && length >= 0
        && length <= Settings.propProgramPanelShortDurationMinutes.getInt()) {
      maxDescLines = 0;
      mDescriptionIcon.setText("");
      mDescriptionIcon.setMaximumLineCount(0);
    } else {
      maxDescLines = Settings.propProgramPanelMaxLines.getInt();
    }
    int additionalHeight = Settings.propProgramPanelUsesExtraSpaceForMarkIcons.getBoolean() && program.getMarkerArr().length > 0 ? 16 : 0;

    if (maxHeight != -1) {
      maxDescLines = (maxHeight - titleHeight - mPictureAreaIcon.getIconHeight() - additionalHeight - V_GAP) / mNormalFont.getSize();
    }

    if (programChanged
        || (maxDescLines != mDescriptionIcon.getMaximumLineCount())) {
      int descHeight = 0;
      // (Re)set the description text
      if (!mSettings.isShowingOnlyDateAndTitle() && program.getProgramState() == Program.IS_VALID_STATE && maxDescLines > 0) {
        mDescriptionIcon.setMaximumLineCount(maxDescLines);
        ProgramFieldType[] infoFieldArr = Settings.propProgramInfoFields
            .getProgramFieldTypeArray();
        String[] infoFieldSeparatorArr = Settings.propProgramInfoFieldsSeparators.getStringArray();
        Reader infoReader = new MultipleFieldReader(program, infoFieldArr, infoFieldSeparatorArr);
        try {
          mDescriptionIcon.setText(infoReader);
        } catch (IOException exc) {
          mLog.log(Level.WARNING, "Reading program info failed for " + program,
              exc);
        }
        descHeight = mDescriptionIcon.getIconHeight();
      } else {
        descHeight = 0;
      }

      int height = titleHeight + descHeight + mPictureAreaIcon.getIconHeight() + additionalHeight + V_GAP;
      
      // Calculate the height
      mHeight = (mChannelLabel != null ? Math.max(height, mChannelLabel.getPreferredSize().height) : height);
      setPreferredSize(new Dimension(WIDTH_TOTAL, mHeight));

      // Calculate the preferred height
      mPreferredHeight = titleHeight + (maxDescLines * mNormalFont.getSize()) + mPictureAreaIcon.getIconHeight() + additionalHeight + V_GAP;

      if (mHeight < mPreferredHeight) {
        mPreferredHeight = mHeight;
      }
    }

    if (isShowing()) {
      if (oldProgram != null) {
        oldProgram.removeChangeListener(this);
      }
      mProgram.addChangeListener(this);
      revalidate();
      repaint();
    }
  }
  
  private boolean showPicture(Program program, boolean dontShow) {
    if (!mSettings.isShowingOnlyDateAndTitle() && program.getProgramState() == Program.IS_VALID_STATE
        && mProgram.hasFieldValue(ProgramFieldType.PICTURE_TYPE)
        && (
        mSettings.isShowingPictureEver() || !dontShow ||
        (mSettings.isShowingPictureInTimeRange() &&
         !ProgramUtilities.isNotInTimeRange(mSettings.getPictureTimeRangeStart(),mSettings.getPictureTimeRangeEnd(),program)) ||
         (mSettings
            .isShowingPictureForDuration() && mSettings.getDuration() <= program.getLength())
         )) {
      dontShow = false;
    }
    
    return !dontShow;
  }

  /**
   * Gets the plugin icons for a program to show them on the left below the start time.
   *
   * @param program
   *          The program to get the icons for.
   * @return The icons for the program.
   */
  private Icon[] getPluginIcons(final Program program) {
    String[] iconPluginArr = Settings.propProgramTableIconPlugins
        .getStringArray();

    if (program.getProgramState() != Program.IS_VALID_STATE || (iconPluginArr == null) || (iconPluginArr.length == 0)) {
      return new Icon[0];
    } else {
      PluginProxyManager mng = PluginProxyManager.getInstance();
      ArrayList<Icon> iconList = new ArrayList<Icon>();
      int info = program.getInfo();
      int[] infoBits = ProgramInfoHelper.getInfoBits();
      Icon[] infoIcons = ProgramInfoHelper.getInfoIcons();
      for (int pluginIdx = 0; pluginIdx < iconPluginArr.length; pluginIdx++) {

        if (iconPluginArr[pluginIdx].equals(Settings.INFO_ID)) {
          // old style format (with all icons)
          if ((info != -1) && (info != 0)) {
            for (int i = 0; i < infoBits.length; i++) {
              if (ProgramInfoHelper.bitSet(info,
                  infoBits[i])
                  && (infoIcons[i] != null)) {
                iconList.add(infoIcons[i]);
              }
            }
          }
        } else if (iconPluginArr[pluginIdx].equals(Settings.PICTURE_ID)) {
          // picture icon
          if (mProgram.hasFieldValue(ProgramFieldType.PICTURE_TYPE)) {
            iconList.add(InfoThemeLoader.getInstance().getIconThemeForIDOrDefault(Settings.propInfoIconThemeID.getString()).getInfoIcon(InfoIconTheme.INFO_HAS_PICTURE));
          }
        } else if (iconPluginArr[pluginIdx].startsWith("FORMAT")) {
          // new style format (each icon separately)
          if ((info != -1) && (info != 0)) {
            int index = Integer.valueOf(iconPluginArr[pluginIdx].substring(7));
            if (ProgramInfoHelper.bitSet(info,
                infoBits[index])
                && (infoIcons[index] != null)) {
              iconList.add(infoIcons[index]);
            }
          }
        } else {
          // plugin icons
          PluginProxy plugin = mng.getPluginForId(iconPluginArr[pluginIdx]);

          // Check whether this entry still uses the old class name
          if (plugin == null) {
            String asId = "java." + iconPluginArr[pluginIdx];
            plugin = mng.getPluginForId(asId);

            if (plugin != null) {
              // It was the old class name, not an ID
              // -> Change the class name to an ID and save it
              iconPluginArr[pluginIdx] = asId;
              Settings.propProgramTableIconPlugins
                  .setStringArray(iconPluginArr);
            }
          }

          // Now add the icons
          if ((plugin != null) && plugin.isActivated()) {
            Icon[] iconArr = plugin.getProgramTableIcons(program);
            if (iconArr != null) {
              // Add the icons
              for (Icon icon : iconArr) {
                iconList.add(icon);
              }
            }
          }
        }
      }

      // Convert the list to an array and return it
      return iconList.toArray(new Icon[iconList.size()]);
    }
  }
  
  /**
   * Paints the component.
   *
   * @param g
   *          The graphics context to paint to.
   */
  public void paintComponent(Graphics g) {
    // lazy update of plugin icons and layout
    if (mHasChanged) {
      mIconArr = getPluginIcons(mProgram);
      mProgramImportance = !mSettings.isIgnoringProgramImportance() ? ProgramUtilities.getProgramImportance(mProgram) : Program.MAX_PROGRAM_IMPORTANCE;
      mHasChanged = false;
    }

    /* Prevent accidentally set program importance to take effect */
    if(mSettings.isIgnoringProgramImportance() || mProgram.getProgramState() == Program.WAS_DELETED_STATE) {
      mProgramImportance = Program.MAX_PROGRAM_IMPORTANCE;
    }

    /* This is for debugging of the marking problem after an data update */
    if(mProgram.getProgramState() == Program.WAS_DELETED_STATE) {
      setForeground(Color.red);
      mTextColor = Color.red;
    } else if(mProgram.getProgramState() == Program.WAS_UPDATED_STATE) {
      setForeground(Color.blue);
      mTextColor = Color.blue;
    }

    int width = getWidth();
    if (getTextIconWidth(width) != mDescriptionIcon.getIconWidth()) {
      recreateTextIcons(width);
    }

    int height = USE_FULL_HEIGHT ? getHeight() : mHeight;
    Graphics2D grp = (Graphics2D) g;

    // if program table is anti-aliased, then this program panel too
    if (Settings.propEnableAntialiasing.getBoolean()) {
      grp.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
          RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    }
    
    Color lightBackground = Settings.propProgramTableColorOnAirLight.getColor();
    Color darkBackground = Settings.propProgramTableColorOnAirDark.getColor();
    int minutesAfterMidnight = IOUtilities.getMinutesAfterMidnight();
    
    if(mMarkTime >= 0 && Settings.propScrollToTimeMarkingActivated.getBoolean()) {
      minutesAfterMidnight = mMarkTime;
      lightBackground = Settings.propScrollToTimeProgramsLightBackground.getColor();
      darkBackground = Settings.propScrollToTimeProgramsDarkBackground.getColor();
    }
    
    // Draw the background if this program is on air
    if (mProgram.isOnAir() || mMarkTime >= 0 && Settings.propScrollToTimeMarkingActivated.getBoolean()) {
     // int minutesAfterMidnight = IOUtilities.getMinutesAfterMidnight();
      int progLength = mProgram.getLength();
      int startTime = mProgram.getStartTime();
      int elapsedMinutes = minutesAfterMidnight - startTime;
      if (elapsedMinutes < 0) {
        // The next day has begun -> we have to add 24 * 60 minutes
        // Example: Start time was 23:50 = 1430 minutes after midnight
        // now it is 0:03 = 3 minutes after midnight
        // elapsedMinutes = (24 * 60) + 3 - 1430 = 13 minutes
        elapsedMinutes += 24 * 60;
      }

      // elapsed minutes can not be larger than run time
      if (progLength > 0) {
        if (elapsedMinutes > progLength) {
          mLog.severe("paint program panel: elapsed minutes to large; sT=" + startTime + " mAM=" + minutesAfterMidnight + " pL=" + progLength + " eM=" + elapsedMinutes);
        }
        elapsedMinutes = Math.min(elapsedMinutes, progLength);
      }

      int borderWidth = Settings.propProgramTableOnAirProgramsShowingBorder
          .getBoolean() ? 1 : 0;
      if (mAxis == ProgramPanelSettings.X_AXIS) {
        // horizontal filling panel
        int progressX = 0;
        if (progLength > 0) {
          progressX = elapsedMinutes * width / progLength;
        }

        Color c = darkBackground;
        grp.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), (int)(c.getAlpha()*mProgramImportance/10.)));

        int fillWidth = progressX - borderWidth;
        if (fillWidth > 0) {
          grp.fillRect(borderWidth, borderWidth, fillWidth, height - borderWidth);
        }
        c = lightBackground;
        grp.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), (int)(c.getAlpha()*mProgramImportance/10.)));

        fillWidth = width - progressX - borderWidth * 2;
        if (fillWidth > 0) {
          grp.fillRect(progressX, borderWidth, fillWidth, height - borderWidth);
        }

      } else {
        // vertical filling panel
        int progressY = 0;
        if (progLength > 0) {
          progressY = elapsedMinutes * height / progLength;
        }

        Color c = darkBackground;
        grp.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), (int)(c.getAlpha()*mProgramImportance/10.)));

        int fillHeight = progressY - borderWidth;

        if (fillHeight > height) {
          mLog.severe("paint program panel: fill height 1 to large");
          fillHeight = height;
        }

        if (fillHeight > 0) {
          grp.fillRect(borderWidth, borderWidth, width - borderWidth * 2, fillHeight);
        }

        c = lightBackground;
        grp.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), (int)(c.getAlpha()*mProgramImportance/10.)));

        fillHeight = height - progressY - borderWidth;

        if (fillHeight > height) {
          mLog.severe("paint program panel: fill height 2 to large");
          fillHeight = height;
        }

        if (fillHeight > 0) {
          grp.fillRect(borderWidth, progressY, width - borderWidth * 2, fillHeight);
        }
      }
      if (Settings.propProgramTableOnAirProgramsShowingBorder.getBoolean()) {
        grp.draw3DRect(0, 0, width - 1, height - 1, true);
      }
    }

    // If there are plugins that have marked the program -> paint the background
    Marker[] markedByPluginArr = mProgram.getMarkerArr();
    if (markedByPluginArr.length != 0) {
      Color c = Plugin.getPluginManager().getTvBrowserSettings().getColorForMarkingPriority(mProgram.getMarkPriority());

      if(c == null) {
        c = Settings.propProgramPanelMarkedMinPriorityColor.getColor();
      }

      int alphaValue = (int)(c.getAlpha()*mProgramImportance/10.);

      if(mProgram.isExpired()) {
        grp.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), (int)(alphaValue*6/10.)));
      }
      else {
        grp.setColor(new Color(c.getRed(), c.getGreen(), c.getBlue(), alphaValue));
      }

      if(mProgram.getMarkPriority() > Program.NO_MARK_PRIORITY) {
        if(Settings.propProgramPanelWithMarkingsShowingBoder.getBoolean()) {
          grp.fill3DRect(0, 0, width, height, true);
        }
        else {
          grp.fillRect(0, 0, width, height);
        }
      }
    }

    if (mMouseOver || mIsSelected) {
      Color test = Settings.propProgramTableMouseOverColor.getColor();
      if (mIsSelected) {
        test = Settings.propKeyboardSelectedColor.getColor();
      }
      grp.setColor(test);
      grp.fillRect(0, 0, width - 1, height - 1);

      Stroke str = grp.getStroke();
      Color col = grp.getColor();
      float[] dash = { 2.0f };
      int lineWidth = 1;
      BasicStroke dashed = new BasicStroke(lineWidth, BasicStroke.CAP_BUTT,
          BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
      
      if(Settings.propTableBackgroundStyle.getString().contains("ui")) {
        grp.setColor(UIManager.getColor("List.foreground"));
      }
      else {
        grp.setColor(Settings.propProgramPanelForegroundColor.getColor());
      }
      
      grp.setStroke(dashed);

      grp.drawRect(lineWidth - 1, lineWidth - 1, width - lineWidth, height - lineWidth);

      grp.setStroke(str);
      grp.setColor(col);
    }
    
    int channelWidth = 0;
    
    if(mChannelLabel != null) {
      try {
        int yTranslation = getHeight()/2-mChannelLabel.getHeight()/2;
            
        grp.translate(1, yTranslation);
        
        mChannelLabel.paint(grp);
        channelWidth = 46;
        grp.translate(channelWidth, -yTranslation);
      }catch(Exception e) {
        mLog.log(Level.SEVERE, "Error in drawing channel logo for program '" + mProgram + "'", e);
      }
    }

    // Draw all the text
    if (mPaintExpiredProgramsPale && mProgram.isExpired()) {
      Color c = new Color(Color.gray.getRed(), Color.gray.getGreen(), Color.gray.getBlue(), (int)(Color.gray.getAlpha()*mProgramImportance/10.));

      setForeground(c);
      grp.setColor(c);
    } else {
      Color c = new Color(mTextColor.getRed(), mTextColor.getGreen(), mTextColor.getBlue(), (int)(mTextColor.getAlpha()*mProgramImportance/10.));

      setForeground(c);
      grp.setColor(c);
    }
        
    grp.setFont(ProgramPanel.mTimeFont);
    grp.drawString(mProgramTimeAsString, 1, mTimeFont.getSize());
    
    mTitleIcon.paintIcon(this, grp, WIDTH_LEFT, 0);

    if (!mSettings.isShowingOnlyDateAndTitle() && mProgram.getProgramState() == Program.IS_VALID_STATE) {
      mPictureAreaIcon.paintIcon(this,grp, WIDTH_LEFT, mTitleIcon.getIconHeight());

      if (mHeight >= mPreferredHeight) {
        mDescriptionIcon.paintIcon(this, grp, WIDTH_LEFT, mTitleIcon
            .getIconHeight() + mPictureAreaIcon.getIconHeight());
      }

      // Paint the icons pale if the program is expired
      if (mPaintExpiredProgramsPale && mProgram.isExpired() && mProgramImportance == 10) {
        grp.setComposite(PALE_COMPOSITE);
      }
      else if (mProgramImportance != 10) {
        grp.setComposite(AlphaComposite.getInstance(
            AlphaComposite.SRC_OVER, mProgramImportance/10F));
      }
    }


    // paint the icons of the plugins that have marked the program (lower right corner)
    int x = width - 1 - mLogoWidth;
    int y = mTitleIcon.getIconHeight() + mDescriptionIcon.getIconHeight()
        + mPictureAreaIcon.getIconHeight() + 18;
    y = Math.min(y, height - 1);
    for (Marker marker: markedByPluginArr) {
      Icon[] icons = marker.getMarkIcons(mProgram);
      if (icons != null) {
        for(int i = icons.length - 1; i >= 0 ; i--) {
          x -= icons[i].getIconWidth();

          icons[i].paintIcon(this, grp, x, y - icons[i].getIconHeight());
        }
      }
    }

    // Paint the icons on the left side
    if (mIconArr != null) {
      x = ICON_DISTANCE_X;
      y = mTimeFont.getSize() + 3;
      Point iconsTopLeft = new Point(x, y);

      // calculate height with double column layout
      int sumHeights = -ICON_DISTANCE_Y;
      int maxWidth = 0;
      int rowWidth = 0;
      for (int i = 0; i < mIconArr.length; i++) {
        sumHeights += mIconArr[i].getIconHeight() + ICON_DISTANCE_Y;
        if (i % 2 == 0) {
          rowWidth = mIconArr[i].getIconWidth();
        }
        else {
          rowWidth += mIconArr[i].getIconWidth();
        }
        if (rowWidth > maxWidth) {
          maxWidth = rowWidth;
        }
      }

      // single column of icons
      int colCount = 1;
      // layout icons in pairs
      if ((y + sumHeights >= mHeight) && (maxWidth + 3 * ICON_DISTANCE_X < WIDTH_LEFT)) {
        colCount = 2;
      }
      int iconHeight = 0;
      int currentX = x;
      for (int i = 0; i < mIconArr.length; i++) {
        Icon icon = mIconArr[i];
        boolean nextColumn = (colCount == 1) || (i % 2 == 0);
        if (nextColumn) {
          currentX = x;
          iconHeight = icon.getIconHeight();
        }
        else {
          iconHeight = Math.max(iconHeight, icon.getIconHeight());
        }
        if ((y + iconHeight) < mHeight) {
          icon.paintIcon(this, grp, currentX, y);
        }
        if (nextColumn) {
          currentX += icon.getIconWidth() + ICON_DISTANCE_X;
        }
        if (!nextColumn || (colCount == 1)) {
          y += iconHeight + ICON_DISTANCE_Y;
        }
      }
      // remember the size of this area for tooltip
      if (colCount == 1) {
        mInfoIconRect = new Rectangle(iconsTopLeft.x + channelWidth, iconsTopLeft.y, currentX
            - iconsTopLeft.x, y - iconsTopLeft.y);
      } else {
        mInfoIconRect = new Rectangle(iconsTopLeft.x + channelWidth, iconsTopLeft.y, maxWidth,
            y - iconsTopLeft.y + iconHeight);
      }
    }

    // Reset the old composite
    if (mPaintExpiredProgramsPale && mProgram.isExpired()) {
      grp.setComposite(NORMAL_COMPOSITE);
    }
  }

  /**
   * Called when the panel is added to a container.
   * <p>
   * registers the panel as ChangeListener at the program.
   */
  public void addNotify() {
    super.addNotify();
    mProgram.addChangeListener(this);
    PluginProxyManager.getInstance().addPluginStateListener(this);
  }

  /**
   * Called when the panel is added to a container.
   * <p>
   * removes the panel as ChangeListener from the program.
   */
  public void removeNotify() {
    super.removeNotify();
    mProgram.removeChangeListener(this);
    PluginProxyManager.getInstance().removePluginStateListener(this);
  }

  /**
   * Gets the program object of this ProgramPanel.
   *
   * @return the program object of this ProgramPanel.
   */
  public Program getProgram() {
    return mProgram;
  }

  /**
   * Adds a MouseListener that shows the plugin context menu when the user does
   * a right click on the program panel.
   *
   * @param caller
   *          The ContextMenuIf to exclude from the context menu. If
   *          <code>null</code> no ContextMenuIf is excluded.
   */
  public void addPluginContextMenuMouseListener(final ContextMenuIf caller) {
    ProgramMouseAndContextMenuListener listener = new ProgramMouseAndContextMenuListener() {
      @Override
      public void showContextMenu(MouseEvent e) {
        showPopup(e, caller);
      }
      
      @Override
      public void mouseEventActionFinished() {}
      
      @Override
      public Program getProgramForMouseEvent(MouseEvent e) {
        return mProgram;
      }
    };
    ProgramMouseEventHandler mouseHandler = new ProgramMouseEventHandler(listener, caller);
    
    addMouseListener(mouseHandler);
  }

  /**
   * Shows the Popup
   *
   * @param evt
   *          Event for X/Y-Coordinates
   * @param caller
   *          ContextMenuIf that called this
   */
  private void showPopup(MouseEvent evt, ContextMenuIf caller) {
    if (SwingUtilities.isRightMouseButton(evt)) {
      JPopupMenu menu = PluginProxyManager.createPluginContextMenu(mProgram,
          caller);
      menu.show(evt.getComponent(), evt.getX() - 15, evt.getY() - 15);
    }
  }

  /**
   * Should be called, when the program has changed.
   * <p>
   * If you use this program panel directly (this is the case, when you have
   * added it into a JPanel), you don't have to call this method. This is done
   * automatically.
   * <p>
   * But if you use this panel just as renderer (e.g. for a list) you have to
   * register at the programs as ChangeListener and call this method when
   * {@link ChangeListener#stateChanged(javax.swing.event.ChangeEvent)} was
   * called.
   *
   * @see Program#addChangeListener(ChangeListener)
   */
  public void programHasChanged() {
    // we need to remember changed state so we can update the plugin icons
    // before painting
    mHasChanged = true;
  }

  // implements ChangeListener

  /**
   * Called when the state of the program has changed.
   * <p>
   * repaints the panel.
   *
   * @param evt
   *          The event describing the change.
   */
  public void stateChanged(ChangeEvent evt) {
    if (mProgram.equals(evt.getSource())) {
      programHasChanged();
      repaint();
    }
  }

  /**
   * Sets the Color of the Text
   *
   * @param col
   *          Color of the Text
   */
  public void setTextColor(Color col) {
    mTextColor = col;
  }

  /**
   * Returns the Color of the Text
   *
   * @return Color of the Text
   */
  public Color getTextColor() {
    return mTextColor;
  }

  /**
   * Paints the ProgramPanel
   *
   * @param mouse
   *          under a Mouse and needs highlight?
   * @param isSelected
   *          IsSelected program?
   * @param g
   *          Graphics-Object
   *
   */
  public void paint(boolean mouse, boolean isSelected, Graphics g) {
    mMouseOver = mouse;
    mIsSelected = isSelected;
    super.paint(g);
  }

  /**
   * @return The smallest height possible.
   */
  public int getMinimumHeight() {
    return mTitleIcon.getIconHeight() + mPictureAreaIcon.getIconHeight() + 3 + (Settings.propProgramPanelUsesExtraSpaceForMarkIcons.getBoolean() && mProgram.getMarkerArr().length > 0 ? 16 : 0);
  }

  /**
   * Sets if expired programs should be painted pale.
   * <p>
   * @param value <code>True</code> if expired programs should be painted pale,
   * <code>false</code> otherwise.
   * @since 2.7
   */
  public void setPaintExpiredProgramsPale(boolean value) {
    mPaintExpiredProgramsPale = value;
  }

  public void pluginActivated(PluginProxy plugin) {
    if (plugin.getProgramTableIcons(Plugin.getPluginManager().getExampleProgram()) != null) {
      updatePrograms();
    }
  }

  public void pluginDeactivated(PluginProxy plugin) {
    updatePrograms();
  }

  private void updatePrograms() {
    programHasChanged();
    repaint();
  }

  public void pluginLoaded(PluginProxy plugin) {
    // noop
  }

  public void pluginUnloaded(PluginProxy plugin) {
    // noop
  }

  /**
   * get the tooltip text for the local mouse coordinates x and y
   *
   * @param x
   * @param y
   * @return tooltip text or null, if there is nothing "special" at the coordinates
   */
  public String getToolTipText(int x, int y) {
    // tooltip for info icon area (on the left)
    if (mInfoIconRect != null && mInfoIconRect.contains(x, y)) {
      return getProgramInfoTooltip();
    }

    // tooltip for all marker icons
    Marker[] markers = mProgram.getMarkerArr();
    if (markers != null && markers.length > 0
        && x >= getWidth() - markers.length * 16 - 2) {
      int markerY = mTitleIcon.getIconHeight()
          + mDescriptionIcon.getIconHeight() + mPictureAreaIcon.getIconHeight()
          + 2;
      markerY = Math.min(markerY, getHeight() - 16);
      if ((y >= markerY) && (y <= markerY + 16)) {
        return getMarkedByTooltip();
      }
    }

    // show full title if the title has been shortened
    if (mTitleIcon.isTextCut()) {
      return mProgram.getTitle();
    }

    return null;
  }

  private String getMarkedByTooltip() {
    StringBuilder buffer = new StringBuilder();
    Marker[] markers = mProgram.getMarkerArr();
    for (int i = markers.length - 1; i >= 0; i--) {
      Marker marker = markers[i];
      String text = "";
      PluginAccess plugin = Plugin.getPluginManager()
          .getActivatedPluginForId(marker.getId());
      if (plugin != null) {
        text = plugin.getInfo().getName();
      } else {
        InternalPluginProxyIf internalPlugin = InternalPluginProxyList
            .getInstance().getProxyForId(marker.getId());
        if (internalPlugin != null) {
          text = internalPlugin.getName();
          if (internalPlugin.equals(FavoritesPluginProxy.getInstance())) {
            // if this is a favorite, add the names of the favorite
            StringBuilder favTitles = new StringBuilder();
            for (Favorite favorite : FavoriteTreeModel.getInstance()
                .getFavoritesContainingProgram(mProgram)) {
              if (favTitles.length() > 0) {
                favTitles.append(", ");
              }
              favTitles.append(favorite.getName());
            }
            if (favTitles.length() > 0) {
              text += " (" + favTitles.toString() + ')';
            }
          }
        }
      }
      if (text.length() > 0) {
        buffer.append(text);
        buffer.append("<br/>");
      }
    }
    if (buffer.length() > 0) {
      buffer.insert(0, "<html><b>" + mLocalizer.msg("markedBy", "Marked by")
          + "</b><br/>");
      buffer.append("</html>");
      return buffer.toString();
    }
    return null;
  }

  private String  getProgramInfoTooltip() {
    StringBuilder buffer = new StringBuilder("");
    // program info icons
    int info = mProgram.getInfo();
    if (info > 0) {
      int[] infoBitArr = ProgramInfoHelper.getInfoBits();
      Icon[] infoIcons = ProgramInfoHelper.getInfoIcons();
      String[] infoMsgArr = ProgramInfoHelper.getInfoIconMessages();
      
      for (int i = 0; i < infoBitArr.length; i++) {
        if (ProgramInfoHelper.bitSet(info, infoBitArr[i])) {
          if (infoIcons[i] != null) {
            buffer.append("<tr><td valign=\"middle\" align=\"center\"><img src=\"")
                .append(infoIcons[i]).append("\"></td><td>&nbsp;")
                .append(infoMsgArr[i]).append("</td></tr>");
          }
        }
      }
    }
    // plugin icons
    for (PluginProxy proxy : PluginProxyManager.getInstance().getActivatedPlugins()) {
      Icon[] pluginIcons = proxy.getProgramTableIcons(mProgram);
      String singleText = proxy.getProgramTableIconText();
      
      ToolTipIcon[] toolTipIcons = proxy.getProgramTableToolTipIcons(mProgram);
      
      if (toolTipIcons == null && pluginIcons != null && pluginIcons.length > 0 && singleText != null) {
          buffer.append("<tr><td valign=\"middle\" align=\"center\"></td><td>&nbsp;")
            .append(singleText).append("</td></tr>");
      }
      else if(toolTipIcons != null) {
        for(ToolTipIcon icons : toolTipIcons) {
          icons.append(buffer);
        }
      }
    }
    // HTML header and footer
    if (buffer.length() > 0) {
      buffer.insert(0, "<html><table cellpadding=\"1\">");
      buffer.append("</table></html>");
      return buffer.toString();
    }
    return null;
  }

  /**
   * get the X offset of the title in pixels
   *
   * @return X offset of the title in pixels
   */
  public int getTitleX() {
    return WIDTH_LEFT;
  }

  @Override
  public String getToolTipText(MouseEvent event) {
    return getToolTipText(event.getX(), event.getY());
  }

  @Override
  public void setSize(Dimension d) {
    super.setSize(d);
    setWidth(d.width);
  }

  @Override
  public void setSize(int width, int height) {
    super.setSize(width, height);
    setWidth(width);
  }

  public void setWidth(int newWidth) {
    if(newWidth > Settings.MIN_COLUMN_WIDTH) {
      int oldWidth = getWidth();
      
      int textIconWidth = getTextIconWidth(newWidth);
      if (oldWidth != newWidth && newWidth > 0 || (textIconWidth != mDescriptionIcon.getIconWidth() && textIconWidth > 0)) {
        recreateTextIcons(newWidth);
      }
    }
  }

  private int getTextIconWidth(final int fullWidth) {
    return fullWidth - WIDTH_LEFT - 5 - mLogoWidth;
  }

  int textIconWidth = -1;
  
  /**
   * recreate the textual icons if the width of the program panel has changed
   * @param newWidth
   */
  private void recreateTextIcons(int newWidth) {
    textIconWidth = getTextIconWidth(newWidth);
    mTitleIcon = new TextAreaIcon(null, mTitleFont, textIconWidth);
    mDescriptionIcon = new TextAreaIcon(null, mNormalFont, textIconWidth);
    mDescriptionIcon.setMaximumLineCount(3);
    
    if (mProgram != null) {
      mProgram.validateMarking();
      Program p = mProgram;
      mProgram = null;
      setProgram(p);
    }
  }

  public void setMarkTime(int time) {
    mMarkTime = time;
  }

}