/*
 * TV-Browser
 * Copyright (C) 04-2003 Martin Oberhauser (martin@tvbrowser.org)
 *
 * 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 tvbrowser.core.plugin;

import java.awt.Frame;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Properties;
import java.util.logging.Logger;

import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JOptionPane;

import tvbrowser.TVBrowser;
import tvbrowser.core.PluginLoader;
import tvbrowser.core.Settings;
import tvbrowser.core.filters.GenericFilterMap;
import tvbrowser.core.filters.UserFilter;
import tvbrowser.ui.mainframe.MainFrame;
import tvdataservice.MutableChannelDayProgram;
import util.exc.TvBrowserException;
import util.io.IOUtilities;
import util.io.stream.ObjectOutputStreamProcessor;
import util.io.stream.StreamUtilities;
import util.ui.Localizer;
import util.ui.TVBrowserIcons;
import util.ui.UiUtilities;
import devplugin.ActionMenu;
import devplugin.AfterDataUpdateInfoPanel;
import devplugin.Channel;
import devplugin.ChannelDayProgram;
import devplugin.ContextMenuAction;
import devplugin.Date;
import devplugin.ImportanceValue;
import devplugin.Plugin;
import devplugin.PluginCenterPanelWrapper;
import devplugin.PluginCommunication;
import devplugin.PluginInfo;
import devplugin.PluginTreeNode;
import devplugin.PluginsFilterComponent;
import devplugin.PluginsProgramFilter;
import devplugin.Program;
import devplugin.ProgramRatingIf;
import devplugin.ProgramReceiveTarget;
import devplugin.SettingsTab;
import devplugin.ToolTipIcon;

/**
 * A plugin proxy for Java plugins.
 *
 * @author Til Schneider, www.murfman.de
 */
public class JavaPluginProxy extends AbstractPluginProxy {

  /** The logger for this class */
  private static final Logger mLog
    = Logger.getLogger(JavaPluginProxy.class.getName());

  /** The plugin itself. */
  private Plugin mPlugin;

  /** The ID of this plugin. */
  private String mId;

  private PluginInfo mPluginInfo;

  private String mPluginFileName;

  /** plugin icon, only used if the plugin is not active */
  private Icon mPluginIcon;

  /**
   * file name of the icon of this plugin proxy, used for lazy loading
   */
  private String mIconFileName;
  
  private String mPluginCategory;


  public JavaPluginProxy(Plugin plugin, String pluginFileName) {
    mPlugin = plugin;
    mPluginFileName = pluginFileName;
  }

  public JavaPluginProxy(PluginInfo info, String pluginFileName, String pluginId, Icon pluginIcon, String category) {
    mPluginInfo = info;
    mPluginFileName = pluginFileName;
    mId = pluginId;
    mPluginIcon = pluginIcon;
    mPluginCategory = category;
  }

  public JavaPluginProxy(PluginInfo info, String pluginFileName, String pluginId, String iconFileName, String category) {
    mPluginInfo = info;
    mPluginFileName = pluginFileName;
    mId = pluginId;
    mIconFileName = iconFileName;
    mPluginCategory = category;
  }

  /**
   * Gets the ID of the given Java plugin.
   *
   * @param javaPlugin The Java plugin to get the ID for.
   * @return The ID of the given Java plugin.
   */
  public static String getJavaPluginId(Plugin javaPlugin) {
    return "java." + javaPlugin.getClass().getName();
  }


  /**
   * Gets the ID of this plugin.
   *
   * @return The ID of this plugin.
   */
  public String getId() {
    if (mId == null) {
      mId = getJavaPluginId(mPlugin);
    }
    return mId;
  }


  /**
   * Sets the parent frame to the plugin.
   *
   * @param parent The parent frame to set.
   */
  void setParentFrame(Frame parent) {
    if (mPlugin != null) {
      mPlugin.setParent(parent);
    }
  }


  /**
   * Really loads the settings for this plugin.
   *
   * @param userDirectory The directory where the user data is stored.
   * @throws TvBrowserException If loading failed.
   */
  protected void doLoadSettings(File userDirectory) throws TvBrowserException {
    String pluginClassName = mPlugin.getClass().getName();

    // Get all the file names
    File oldDatFile = new File(userDirectory, pluginClassName + ".dat");
    File oldPropFile = new File(userDirectory, pluginClassName + ".prop");
    File datFile = new File(userDirectory, getId() + ".dat");
    File propFile = new File(userDirectory, getId() + ".prop");

    // Rename the old data and settings file if they still exist
    oldDatFile.renameTo(datFile);
    oldPropFile.renameTo(propFile);

    // load plugin data
    if (datFile.exists()) {
      ObjectInputStream in = null;
      try {
        in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(datFile), 0x4000));
        mPlugin.readData(in);
      }
      catch (Throwable thr) {
        File datFileBackup = new File(userDirectory, getId() + ".dat_old");
        
        if(datFileBackup.isFile()) {
          try {
            in = new ObjectInputStream(new BufferedInputStream(new FileInputStream(datFileBackup), 0x4000));
            mPlugin.readData(in);
            mLog.severe("Date file '" + datFile.getAbsolutePath() + "' could not be read. Read old file instead: '" + datFileBackup.getAbsolutePath() + "'.");
          }catch(Throwable thr1) {
            throw new TvBrowserException(getClass(), "error.3",
                "Loading data for plugin {0} failed.\n({1})",
                getInfo().getName(), datFileBackup.getAbsolutePath(), thr);            
          }
        }
        else {        
          throw new TvBrowserException(getClass(), "error.3",
              "Loading data for plugin {0} failed.\n({1})",
              getInfo().getName(), datFile.getAbsolutePath(), thr);
        }
      }
      finally {
        if (in != null) {
          try { in.close(); } catch (IOException exc) {
            // ignore
          }
        }
      }
    }

    // load plugin settings
    BufferedInputStream in = null;
    try {
      if (propFile.exists()) {
        Properties prop = new Properties();
        in = new BufferedInputStream(new FileInputStream(propFile), 0x4000);
        prop.load(in);
        in.close();
        mPlugin.loadSettings(prop);
      } else {
        mPlugin.loadSettings(new Properties());
      }
    }
    catch (Throwable thr) {
      throw new TvBrowserException(getClass(), "error.4",
          "Loading settings for plugin {0} failed.\n({1})",
          getInfo().getName(), propFile.getAbsolutePath(), thr);
    }
    finally {
      if (in != null) {
        try { in.close(); } catch (IOException exc) {
          // ignore
        }
      }
    }
  }


  /**
   * Really saves the settings for this plugin.
   *
   * @param userDirectory The directory where the user data is stored.
   * @throws TvBrowserException If saving failed.
   */
  protected void doSaveSettings(File userDirectory, boolean log) throws TvBrowserException {
    if(log) {
      mLog.info("Storing plugin settings for " + getId() + "...");
    }

    // save the plugin data in a temp file
    File tmpDatFile = new File(userDirectory, getId() + ".dat.temp");
    File oldDatFile = new File(userDirectory, getId() + ".dat_old");
    
    try {
      StreamUtilities.objectOutputStream(tmpDatFile,
          new ObjectOutputStreamProcessor() {
            public void process(ObjectOutputStream out) throws IOException {
              mPlugin.writeData(out);
              out.close();
            }
          });

      // Saving succeeded -> Delete the old file and rename the temp file
      File datFile = new File(userDirectory, getId() + ".dat");
      
      if(oldDatFile.isFile()) {
        oldDatFile.delete();
      }
      
      datFile.renameTo(oldDatFile);
      tmpDatFile.renameTo(datFile);
    }
    catch(Throwable thr) {
      throw new TvBrowserException(getClass(), "error.5",
          "Saving data for plugin {0} failed.\n({1})",
          getInfo().getName(), tmpDatFile.getAbsolutePath(), thr);
    }

    // save the plugin settings in a temp file
    FileOutputStream fOut = null;
    File tmpPropFile = new File(userDirectory, getId() + ".prop.temp");
    try {
      Properties prop = mPlugin.storeSettings();
      if (prop != null) {
        fOut = new FileOutputStream(tmpPropFile);
        prop.store(fOut, "Settings for plugin " + getInfo().getName());
        fOut.close();
      }

      // Saving succeeded -> Delete the old file and rename the temp file
      File propFile = new File(userDirectory, getId() + ".prop");
      propFile.delete();
      tmpPropFile.renameTo(propFile);
    }
    catch (Throwable thr) {
      throw new TvBrowserException(getClass(), "error.6",
          "Saving settings for plugin {0} failed.\n({1})",
          getInfo().getName(), tmpPropFile.getAbsolutePath(), thr);
    }
    finally {
      if (fOut != null) {
        try { fOut.close(); } catch (IOException exc) {
          // ignore
        }
      }
    }
  }


  /**
   * Really gets the meta information about the plugin.
   *
   * @return The meta information about the plugin.
   */
  protected PluginInfo doGetInfo() {
    if (mPluginInfo != null) {
      return mPluginInfo;
    }

    PluginInfo info = null;

    try {
      info = mPlugin.getInfo();
    }catch(java.lang.NoSuchMethodError e) {
      String name = devplugin.Plugin.mLocalizer.msg( "unkown" ,"Unknown" );
      String desc = devplugin.Plugin.mLocalizer.msg( "noDescription" ,"No description" );
      String author = devplugin.Plugin.mLocalizer.msg( "noAuthor" ,"No author given" );

      return new PluginInfo(devplugin.Plugin.class,name, desc, author);
    }

    return info;
  }


  /**
   * Really gets the SettingsTab object, which is added to the settings-window.
   *
   * @return the SettingsTab object or <code>null</code> if the plugin does not
   *         provide this feature.
   */
  protected SettingsTabProxy doGetSettingsTab() {
    SettingsTab tab = mPlugin.getSettingsTab();
    if (tab == null) {
      return null;
    }
    return new SettingsTabProxy(tab);
  }


  /**
   * Gets the actions for the context menu of a program.
   *
   * @param program The program the context menu will be shown for.
   * @return the actions this plugin provides for the given program or
   *         <code>null</code> if the plugin does not provide this feature.
   */
  protected ActionMenu doGetContextMenuActions(Program program) {
    if(program != null && !program.equals(PluginManagerImpl.getInstance().getExampleProgram())) {
      UserFilter filter = GenericFilterMap.getInstance().getGenericPluginFilter(this, true);
      
      if(accessControl(program.getChannel()) || (filter != null && !filter.accept(program))) {
        return null;
      }
    }
    
    return mPlugin.getContextMenuActions(program);
  }


  /**
   * Gets the actions for the context menu of a channel.
   *
   * @param channel The channel the context menu will be shown for.
   * @return the actions this plugin provides for the given channel or
   *         <code>null</code> if the plugin does not provide this feature.
   */
  protected ActionMenu doGetContextMenuActions(final Channel channel) {
    if(!accessControl(channel)) {
      return mPlugin.getContextMenuActions(channel);
    }
    
    return null;
  }

  /**
   * Really gets the action to use for the main menu and the toolbar.
   *
   * @return the action to use for the menu and the toolbar or <code>null</code>
   *         if the plugin does not provide this feature.
   */
  protected ActionMenu doGetButtonAction() {
    if (mPlugin != null) {
      ActionMenu actMenu = mPlugin.getButtonAction();
      if (actMenu != null) {
        Action action = actMenu.getAction();
        if (action != null && !(action instanceof ContextMenuAction)) {
          if (action.getValue(Action.SMALL_ICON) == null) {
            mLog.warning("Small icon missing for button action "
                + action.getValue(Action.NAME));
            action.putValue(Action.SMALL_ICON, TVBrowserIcons.warning(TVBrowserIcons.SIZE_SMALL));
          }
          if (action.getValue(Plugin.BIG_ICON) == null) {
            mLog.warning("Big icon missing for button action "
                + action.getValue(Action.NAME));
            action.putValue(Plugin.BIG_ICON, TVBrowserIcons.warning(TVBrowserIcons.SIZE_LARGE));
          }
        }
      }
      return actMenu;
    }
    return null;
  }


  /**
   * Really gets the icons to use for marking programs in the program table.
   *
   * @return the icons to use for marking programs in the program table.
   */
  protected Icon[] doGetMarkIcons(Program p) {
    if (mPlugin != null && p != null) {
      UserFilter filter = GenericFilterMap.getInstance().getGenericPluginFilter(this, true);
      
      if(!(accessControl(p.getChannel()) || (filter != null && !filter.accept(p)))) {
        return mPlugin.getMarkIcons(p);
      }
    }
    
    return null;
  }


  /**
   * Gets the description text for the program table icons provided by this
   * Plugin.
   * <p>
   * Return <code>null</code> if your plugin does not provide this feature.
   *
   * @return The description text for the program table icons or
   *         <code>null</code> if the plugin does not provide this feature.
   *
   * @see #getProgramTableIcons(Program)
   */
  protected String doGetProgramTableIconText() {
    return mPlugin.getProgramTableIconText();
  }


  /**
   * Really gets the icons this Plugin provides for the given program. These
   * icons will be shown in the program table under the start time.
   * <p>
   * Return <code>null</code> if your plugin does not provide this feature.
   *
   * @param program The programs to get the icons for.
   * @return The icons for the given program or <code>null</code>.
   *
   * @see #getProgramTableIconText()
   */
  protected Icon[] doGetProgramTableIcons(Program program) {
    if(program != null && !accessControl(program.getChannel())) {
      return mPlugin.getProgramTableIcons(program);
    }
    
    return null;
  }


  /**
   * This method is automatically called, when the TV data update is finished.
   *
   * @see #handleTvDataAdded(ChannelDayProgram)
   * @see #handleTvDataDeleted(ChannelDayProgram)
   */
  protected void doHandleTvDataUpdateFinished() {
    mPlugin.handleTvDataUpdateFinished();
  }


  /**
   * This method is automatically called, when TV data was added.
   * (E.g. after an update).
   * <p>
   * The TV data may be modified by the plugin! So this method must be called
   * before new TV data is saved.
   *
   * @param newProg The new ChannelDayProgram.
   * @see #handleTvDataDeleted(ChannelDayProgram)
   * @see #handleTvDataUpdateFinished()
   */
  protected void doHandleTvDataAdded(ChannelDayProgram newProg) {
    if(newProg != null && !accessControl(newProg.getChannel())) {
      mPlugin.handleTvDataAdded(newProg);
    }
  }


  /**
   * This method is automatically called, when TV data was deleted.
   * (E.g. after an update).
   *
   * @param oldProg The old ChannelDayProgram which was deleted.
   * @see #handleTvDataAdded(ChannelDayProgram)
   * @see #handleTvDataUpdateFinished()
   */
  protected void doHandleTvDataDeleted(ChannelDayProgram oldProg) {
    if(oldProg != null && !accessControl(oldProg.getChannel())) {
      mPlugin.handleTvDataDeleted(oldProg);
    }
  }

  /**
   * This method is automatically called, when the TV-Browser start is complete.
   */
  protected void doHandleTvBrowserStartFinished() {
    mPlugin.handleTvBrowserStartFinished();
  }

  public void doOnActivation() throws Throwable {
    if(!TVBrowser.isSafeMode()) {
      plugin().onActivation();
    }
  }

  private Plugin plugin() {
    if (mPlugin == null) {
      mPlugin = (Plugin) PluginLoader.getInstance().loadPlugin(new File(mPluginFileName), false);
    }
    return mPlugin;
  }

  public void doOnDeactivation() throws Throwable {
    if(mPlugin != null) {
      mPlugin.onDeactivation();
    }
  }

  public boolean doCanUseProgramTree() {
    return mPlugin.canUseProgramTree();
  }

  public PluginTreeNode getRootNode() {
    return mPlugin.getRootNode();
  }

  /**
   * Really gets whether the plugin supports receiving programs from other
   * plugins with target.
   *
   * @return Whether the plugin supports receiving programs from other plugins with target.
   * @see #receivePrograms(Program[],ProgramReceiveTarget)
   * @since 2.5
   */
  protected boolean doCanReceiveProgramsWithTarget() {
    return mPlugin.canReceiveProgramsWithTarget();
  }

  /**
   * Really receives a list of programs from another plugin with target.
   *
   * @param programArr The programs passed from the other plugin with target.
   * @param receiveTarget The target of the programs.
   * @see #canReceiveProgramsWithTarget()
   * @since 2.5
   */
  protected boolean doReceivePrograms(Program[] programArr, ProgramReceiveTarget receiveTarget) {
    if(programArr != null && accessControl()) {
      ArrayList<Program> accessPrograms = new ArrayList<Program>();
      
      for(Program p : programArr) {
        if(!p.getChannel().isAccessControl()) {
          accessPrograms.add(p);
        }
      }
      
      programArr = accessPrograms.toArray(new Program[accessPrograms.size()]);
    }
    
    boolean value = mPlugin.receivePrograms(programArr, receiveTarget);

    if(!value) {
      JOptionPane.showMessageDialog(UiUtilities.getLastModalChildOf(MainFrame.getInstance()),mLocalizer.msg("error.noTarget","The programs for the target \"{0}\" couldn't be processed by \"{1}\".",receiveTarget,mPlugin.getInfo().getName()),Localizer.getLocalization(Localizer.I18N_ERROR),JOptionPane.ERROR_MESSAGE);
    }

    return value;
  }

  /**
   * Really return an array of receive target or <code>null</code> if there is no target
   *
   * @return The supported receive targets.
   * @see #canReceiveProgramsWithTarget()
   * @see #receivePrograms(Program[],ProgramReceiveTarget)
   * @since 2.5
   */
  protected ProgramReceiveTarget[] doGetProgramReceiveTargets() {
    ProgramReceiveTarget[] targets = mPlugin.getProgramReceiveTargets();

    return targets != null ? targets : ProgramReceiveTarget.createDefaultTargetArrayForProgramReceiveIf(mPlugin);
  }

  /**
   * Really returns the available program filters that the plugin supports.
   *
   * @return The available program filters that the plugin supports or <code>null</code> if it supports no filter.
   * @since 2.5
   */
  protected PluginsProgramFilter[] doGetAvailableFilter() {
    return mPlugin.getAvailableFilter();
  }

  /**
   * Really return if a program filter can be deleted.
   *
   * @param programFilter The program filter to delete.
   * @return True if the program filter component can be deleted.
   * @since 2.5
   */
  protected boolean doIsAllowedToDeleteProgramFilter(PluginsProgramFilter programFilter) {
    return mPlugin.isAllowedToDeleteProgramFilter(programFilter);
  }

  /**
   * Really gets the available filter component classes.
   *
   * @return The available plugins filter components classes or <code>null</code> if no plugins filter components are supported.
   * @since 2.5
   */
  protected  Class<? extends PluginsFilterComponent>[] doGetAvailableFilterComponentClasses() {
    return mPlugin.getAvailableFilterComponentClasses();
  }

  /**
   * Really gets the mark priority for the given Program.
   * <p>
   * @param p The program to get the mark priority for.
   * @return The mark priority for the given program.
   */
  protected int doGetMarkPriorityForProgram(Program p) {
    UserFilter filter = GenericFilterMap.getInstance().getGenericPluginFilter(this, true);
    
    if(p != null && (accessControl(p.getChannel()) || (filter != null && !filter.accept(p)))) {
      return Program.NO_MARK_PRIORITY;
    }
    
    return mPlugin.getMarkPriorityForProgram(p);
  }

  public String getPluginFileName() {
    return mPluginFileName;
  }

  /**
   * connect a lazy loaded plugin to its proxy
   * @param plugin
   */
  public void setPlugin(Plugin plugin, String fileName) {
    mPlugin = plugin;
    mPluginFileName = fileName;
  }

  @Override
  public Icon getPluginIcon() {
    // if only the proxy was loaded, then mPluginIcon contains the copied plugin icon
    if (mPluginIcon == null && mIconFileName != null) {
      File iconFile = new File(mIconFileName);
      if (iconFile.canRead()) {
        mPluginIcon = IOUtilities.readImageIconFromFile(iconFile);
      }
    }
    if (mPluginIcon != null) {
      return mPluginIcon;
    }
    // otherwise get the icon as always
    return super.getPluginIcon();
  }

  public ProgramRatingIf[] getProgramRatingIfs() {
    return mPlugin.getRatingInterfaces();
  }

  public String getButtonActionDescription() {
    return mPlugin.getInfo().getDescription();
  }

  @Override
  protected void doHandleTvDataAdded(MutableChannelDayProgram newProg) {
    if(newProg != null && !accessControl(newProg.getChannel())) {
      mPlugin.handleTvDataAdded(newProg);
    }
  }

  @Override
  protected boolean doReceiveValues(String[] values,
      ProgramReceiveTarget receiveTarget) {

    return mPlugin.receiveValues(values,receiveTarget);
  }

  @Override
  protected void doHandleTvDataTouched(ChannelDayProgram removedDayProgram,
      ChannelDayProgram addedDayProgram) {
    if((removedDayProgram != null && !accessControl(removedDayProgram.getChannel())) || 
        (addedDayProgram != null && !accessControl(addedDayProgram.getChannel())) ||
        (removedDayProgram == null && addedDayProgram == null)) {
      mPlugin.handleTvDataTouched(removedDayProgram,addedDayProgram);
    }
  }

  @Override
  protected ImportanceValue doGetImportanceValueForProgram(Program p) {
    UserFilter filter = GenericFilterMap.getInstance().getGenericPluginFilter(this, true);
    
    if(p != null && (accessControl(p.getChannel()) || (filter != null && !filter.accept(p)))) {
      return new ImportanceValue((byte)1,Program.DEFAULT_PROGRAM_IMPORTANCE);
    }
    
    return mPlugin.getImportanceValueForProgram(p);
  }

  @Override
  public String getPluginCategory() {
    if(mPlugin != null) {
      return mPlugin.getPluginCategory();
    }
    
    return mPluginCategory;
  }

  @Override
  public PluginCenterPanelWrapper getPluginCenterPanelWrapper() {
    return mPlugin.getPluginCenterPanelWrapper();
  }

  @Override
  public AfterDataUpdateInfoPanel getAfterDataUpdateInfoPanel() {
    if(mPlugin != null) {
      return mPlugin.getAfterDataUpdateInfoPanel();
    }
    
    return null;
  }

  @Override
  public void handleTvBrowserSettingsChanged() {
    if(mPlugin != null) {
      mPlugin.handleTvBrowserSettingsChanged();
    }
  }

  @Override
  public void handleTvDataUpdateStarted(Date until) {
    if(mPlugin != null) {
      mPlugin.handleTvDataUpdateStarted(until);
    }
  }

  @Override
  public PluginCommunication doGetCommunicationClass() {
    if(mPlugin != null) {
      return mPlugin.getCommunicationClass();
    }
    
    return null;
  }
  
  private boolean accessControl(Channel ch) {
    return (ch != null && ch.isAccessControl() && accessControl());
  }
  
  private boolean accessControl() {
    if(Settings.propAccessControl.getStringArray().length > 0) {
      int startIndex = mPlugin.getId().indexOf(".")+1;
      
      if(Settings.propAccessControl.containsItem(mPlugin.getId().substring(startIndex, mPlugin.getId().indexOf(".", startIndex)))) {
        return true;
      }
    }
    
    return false;
  }

  @Override
  protected boolean doGetAllowsArtificialPluginTree() {
    return mPlugin.isAllowingArtificialPluginTree();
  }

  @Override
  protected ToolTipIcon[] doGetProgramTableToolTipIcons(Program program) {
    return mPlugin.getProgramTableToolTipIcons(program);
  }
}
