/*
 * $Id: BrowserHelpFile.java 4561 2008-11-28 15:57:51Z uckelman $
 *
 * Copyright (c) 2000-2007 by Rodney Kinney
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
package VASSAL.build.module.documentation;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.swing.AbstractAction;
import javax.swing.Action;

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import VASSAL.build.AbstractBuildable;
import VASSAL.build.AutoConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.configure.AutoConfigurer;
import VASSAL.configure.Configurer;
import VASSAL.configure.ConfigurerFactory;
import VASSAL.configure.DirectoryConfigurer;
import VASSAL.configure.VisibilityCondition;
import VASSAL.i18n.ComponentI18nData;
import VASSAL.tools.BrowserSupport;
import VASSAL.tools.WriteErrorDialog;
import VASSAL.tools.io.IOUtils;
import VASSAL.tools.logging.Logger;
import VASSAL.tools.menu.MenuItemProxy;
import VASSAL.tools.menu.MenuManager;

/**
 * Unpacks a zipped directory stored in the module and displays it in an
 * external browser window.
 * 
 * @author rkinney
 */
public class BrowserHelpFile extends AbstractBuildable implements Configurable {
  public static final String TITLE = "title"; //$NON-NLS-1$
  public static final String CONTENTS = "contents"; //$NON-NLS-1$
  public static final String STARTING_PAGE = "startingPage"; //$NON-NLS-1$
  protected String name;
  protected String startingPage;
  protected Action launch;
  protected URL url;
  protected PropertyChangeSupport propSupport = new PropertyChangeSupport(this);
  protected ComponentI18nData myI18nData;

  public BrowserHelpFile() {
    super();
    
    launch = new AbstractAction() {
      private static final long serialVersionUID = 1L;

      public void actionPerformed(ActionEvent e) {
        launch();
      }
    };
  }

  public void launch() {
    if (url == null) {
      extractContents();
    }
    if (url != null) {
      BrowserSupport.openURL(url.toString());
    }
  }
  
  /**
   * The entry in the module Zip file containing the HTML directory 
   * @return
   */
  protected String getContentsResource() {
    return name == null ? null : name.replace(' ', '_');
  }

  protected void extractContents() {
    ZipInputStream in = null;
    try {
      try {
        in = new ZipInputStream(new BufferedInputStream(
              GameModule.getGameModule().getDataArchive()
                .getInputStream("help/" + getContentsResource()))); //$NON-NLS-1$
      }
      catch (IOException e) {
        // The help file was created with empty contents.
        // Assume an absolute URL as the starting page.
        url = new URL(startingPage);
        return;
      }
        
      final File tmp = File.createTempFile("VASSAL", "help"); //$NON-NLS-1$ //$NON-NLS-2$
      File output = tmp.getParentFile();
      tmp.delete();
      output = new File(output, "VASSAL"); //$NON-NLS-1$
      output = new File(output, "help"); //$NON-NLS-1$
      output = new File(output, getContentsResource());
      if (output.exists()) recursiveDelete(output);
  
      output.mkdirs();
      ZipEntry entry;
  
      while ((entry = in.getNextEntry()) != null) {
        if (entry.isDirectory()) {
          new File(output, entry.getName()).mkdirs();
        }
        else {
// FIXME: no way to distinguish between read and write errors here
          FileOutputStream fos = null;
          try {
            fos = new FileOutputStream(new File(output, entry.getName()));
            IOUtils.copy(in, fos);
            in.close();
            fos.close();
          }
          finally {
            IOUtils.closeQuietly(fos);
          }
        }
      }
  
      url = new File(output, startingPage).toURI().toURL();
    }
    // FIXME: review error message
    catch (IOException e) {
      Logger.log(e);
    }
    finally {
      IOUtils.closeQuietly(in);
    }
  }

  protected void recursiveDelete(File output) {
    if (output.isDirectory()) {
      for (File f : output.listFiles()) {
        recursiveDelete(f);
      }
    }
    else {
      output.delete();
    }
  }
 
  public String[] getAttributeNames() {
    return new String[]{
      TITLE,
      CONTENTS,
      STARTING_PAGE
    };
  }

  public String getAttributeValueString(String key) {
    if (TITLE.equals(key)) {
      return name;
    }
    else if (STARTING_PAGE.equals(key)) {
      return startingPage;
    }
    return null;
  }

  public void setAttribute(String key, Object value) {
    if (TITLE.equals(key)) {
      name = (String) value;
      launch.putValue(Action.NAME, name);
      url = null;
      getI18nData().setUntranslatedValue(key, name);
    }
    else if (STARTING_PAGE.equals(key)) {
      startingPage = (String) value;
      url = null;
    }
  }

  protected MenuItemProxy launchItem;

  public void addTo(Buildable parent) {
    launchItem = new MenuItemProxy(launch);
    MenuManager.getInstance().addToSection("Documentation.Module", launchItem);
    launch.setEnabled(true);
  }

  public void removeFrom(Buildable parent) {
    MenuManager.getInstance()
               .removeFromSection("Documentation.Module", launchItem);
    launch.setEnabled(false);
  }

  public void addPropertyChangeListener(PropertyChangeListener l) {
    propSupport.addPropertyChangeListener(l);
  }

  public Class<?>[] getAllowableConfigureComponents() {
    return new Class<?>[0];
  }

  public Configurable[] getConfigureComponents() {
    return new Configurable[0];
  }

  public String getConfigureName() {
    return name;
  }

  public Configurer getConfigurer() {
    return new MyConfigurer(new ConfigSupport());
  }

  public HelpFile getHelpFile() {
    return HelpFile.getReferenceManualPage("HelpMenu.htm","HtmlHelpFile"); //$NON-NLS-1$ //$NON-NLS-2$
  }

  public void remove(Buildable child) {
  }

  public static String getConfigureTypeName() {
    return "HTML Help File"; //$NON-NLS-1$
  }

  /**
   * The attributes we want to expose in the editor are not the same as the ones we want to save to the buildFile, so we
   * use this object to specify the properties in the editor. Also packs up the contents directory and saves it to the
   * ArchiveWriter
   */
  protected class ConfigSupport implements AutoConfigurable {
    public static final String DIR = "dir"; //$NON-NLS-1$
    protected File dir;

    public String[] getAttributeDescriptions() {
      return new String[]{
        "Menu Entry:  ", 
        "Contents:  ",
        "Starting Page:  "
      };
    }

    public String[] getAttributeNames() {
      return new String[]{
        TITLE, DIR,
        STARTING_PAGE
      };
    }

    public Class<?>[] getAttributeTypes() {
      return new Class<?>[]{
        String.class,
        ContentsConfig.class,
        String.class
      };
    }

    public String getAttributeValueString(String key) {
      if (DIR.equals(key)) {
        return dir == null ? null : dir.getPath();
      }
      else {
        return BrowserHelpFile.this.getAttributeValueString(key);
      }
    }

    public VisibilityCondition getAttributeVisibility(String name) {
      return null;
    }

    public void setAttribute(String key, Object value) {
      if (DIR.equals(key)) {
        dir = (File) value;
      }
      else {
        BrowserHelpFile.this.setAttribute(key, value);
      }
    }

    public void packContents() {
      if (dir == null) return;

      File packed = null;
      try {
        packed = File.createTempFile("VASSALhelp", ".zip"); //$NON-NLS-1$ //$NON-NLS-2$
        ZipOutputStream out = null;
        try {
          out = new ZipOutputStream(new FileOutputStream(packed));
          for (File f : dir.listFiles()) {
            packFile(f, "", out); //$NON-NLS-1$
          }
          out.close();
        }
        finally {
          IOUtils.closeQuietly(out);
        }

        GameModule.getGameModule().getArchiveWriter().addFile(
          packed.getPath(),
          "help/" + BrowserHelpFile.this.getContentsResource()); //$NON-NLS-1$
      }
      catch (IOException e) {
        WriteErrorDialog.error(e, packed);
      }
    }

    protected void packFile(File packed, String prefix, ZipOutputStream out)
                                                          throws IOException {
      if (packed.isDirectory()) {
        final ZipEntry entry = new ZipEntry(packed.getName()+"/"); //$NON-NLS-1$
        out.putNextEntry(entry);

        for (File f : packed.listFiles()) {
          packFile(f, prefix + packed.getName() + "/", out); //$NON-NLS-1$
        }
      }
      else {
        final ZipEntry entry = new ZipEntry(prefix + packed.getName());
        out.putNextEntry(entry);

        FileInputStream in = null;
        try {
          in = new FileInputStream(packed); 
          IOUtils.copy(in, out);
          in.close();
        }
        finally {
          IOUtils.closeQuietly(in);
        }
      }
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
      BrowserHelpFile.this.addPropertyChangeListener(l);
    }

    public Class<?>[] getAllowableConfigureComponents() {
      return BrowserHelpFile.this.getAllowableConfigureComponents();
    }

    public Configurable[] getConfigureComponents() {
      return BrowserHelpFile.this.getConfigureComponents();
    }

    public String getConfigureName() {
      return BrowserHelpFile.this.getConfigureName();
    }

    public VASSAL.configure.Configurer getConfigurer() {
      return null;
    }

    public HelpFile getHelpFile() {
      return BrowserHelpFile.this.getHelpFile();
    }

    public void remove(Buildable child) {
    }

    public void removeFrom(Buildable parent) {
    }

    public void add(Buildable child) {
    }

    public void addTo(Buildable parent) {
    }

    public void build(Element e) {
    }

    public Element getBuildElement(Document doc) {
      return null;
    }

    public ComponentI18nData getI18nData() {
      return null;
    }
  }
  public static class ContentsConfig implements ConfigurerFactory {
    public Configurer getConfigurer(AutoConfigurable c, String key, String name) {
      return new DirectoryConfigurer(key, name){
        public Component getControls() {
          Component controls = super.getControls();
          tf.setEditable(false);
          return controls;
        }
      };
    }
  }
  /**
   * Handles the packaging of the target directory into the module file after the user saves the properties in the
   * editor
   * 
   * @author rkinney
   * 
   */
  protected static class MyConfigurer extends AutoConfigurer {
    public MyConfigurer(AutoConfigurable c) {
      super(c);
    }

    public Object getValue() {
      if (target != null) {
        ((ConfigSupport)target).packContents();
      }
      return super.getValue();
    }
  }
  
  public ComponentI18nData getI18nData() {
    if (myI18nData == null) {
      myI18nData = new ComponentI18nData(this, "BrowserHelpFile." + getConfigureName(), null,
          new String[] {TITLE},
          new boolean[] {true},
          new String[] {"Menu Entry:  "});
    }
    return myI18nData;
  }
}
