/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package dvi.browser;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Logger;

import javax.swing.AbstractButton;
import javax.swing.DefaultComboBoxModel;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import dvi.DviException;
import dvi.DviResolution;
import dvi.api.DviContext;
import dvi.api.DviContextSupport;
import dvi.api.DviDocument;
import dvi.browser.util.DviFileWatchTarget;
import dvi.browser.util.FileWatch;
import dvi.ctx.DviToolkit;
import dvi.event.TEvent;
import dvi.event.TEventListener;
import dvi.gui.swing.DefaultDviLayoutManager;
import dvi.gui.swing.DragToScroll;
import dvi.gui.swing.TDviDocument;
import dvi.gui.swing.TexLogViewer;
import dvi.gui.swing.ViewSpec;
import dvi.util.DaemonThreadFactory;
import dvi.util.progress.ManagedProgressItem;
import dvi.util.progress.ProgressItem;
import dvi.util.progress.ProgressRecorder;
import dvi.util.progress.ProgressReporter;

public class DviBrowserPage
extends JPanel
implements DviContextSupport, TEventListener, ActionListener
{
  public static final String CMD_CLOSE_PAGE = "CMD_CLOSE_PAGE";
  public static final String CMD_OPEN_PDF = "CMD_OPEN_PDF";
  public static final String CMD_SET_LAYOUT = "CMD_SET_LAYOUT";
  public static final String CMD_CROP_MARGINS = "CMD_CROP_MARGINS";
  public static final String CMD_TWO_IN_ONE = "CMD_TWO_IN_ONE";
  
  private static final Logger LOGGER = Logger.getLogger(DviBrowserPage.class.getName());
  private JScrollPane scroll = new JScrollPane();
  protected DragToScroll dragToScroll = new DragToScroll(getScrollPane());
  protected TDviDocument tdd;
  
  private ViewSpec viewSpec;
  
  private static final long serialVersionUID = 1L;
  private final DviContextSupport dcs;
  protected TexLogViewer logViewer = new TexLogViewer(5, 64);

  
  private FileWatch.Target watchTarget = null;
  private final ProgressRecorder recorder = new ProgressRecorder(this) {
    @Override
    protected boolean removeEldestElement(ManagedProgressItem item) {
      List<ManagedProgressItem> list = getProgressItems();
      if (list.size() > 100) {
        LOGGER.finer("Remove progress item: " + item);
        return true;
      }
      return false;
    }
  };
  private final ProgressReporter reporter = new ProgressReporter(this);

  private static ExecutorService exe = Executors.newFixedThreadPool(1, new DaemonThreadFactory(Thread.MIN_PRIORITY));
  
  private final DviLoadIndicator loadIndicator = new DviLoadIndicator();

  public DviContext getDviContext()
  {
    return dcs.getDviContext();
  }

  public DviBrowserPage(dvi.api.DviContextSupport dcs) {
    this.dcs = dcs;
    viewSpec = new ViewSpec(this);
    initializeCompontents();
    reporter.addProgressRecorder(getDviContext().getProgressRecorder());
    reporter.addProgressRecorder(recorder);
  }
  
  private URL url;
  private JComboBox layoutSelector;
  private JCheckBox checkBoxCrop;
  private JCheckBox checkBoxTwoInOne;
  private JButton closeButton;
//  private JComboBox zoomSelector;
  
  protected DviDocument openDviDocument(URL url) throws DviException
  {
    return getDviContext().openDviDocument(url);
  }
  
  
  public void open(final URL url)
  throws DviException
  {
    loadIndicator.start();
    try {
      final DviToolkit utils = getDviContext().getDviToolkit();
      final DviDocument doc = openDviDocument(url);
      // TODO: When opening the same document again, check the URL and reload the same page.
      FileWatch.getInstance().removeTarget(watchTarget);
      if ("file".equals(url.getProtocol())) {
        File file = new File(url.getPath());
        watchTarget = new DviFileWatchTarget(this, file);
        FileWatch.getInstance().addTarget(watchTarget);
        watchTarget.getEventModel().addListener(this);
      }
      // Create a defensive copy of view spec.
      final ViewSpec viewSpecCopy = (ViewSpec) getViewSpec().clone();
      final ProgressItem progress = recorder.open("loading " + url);
      exe.submit(new Runnable() {
        public void run()
        {
          try {
            final ProgressItem item = recorder.open("preparing fonts");
            LOGGER.fine("Begin preparing fonts");
            try {
              utils.prepareForRendering(doc, viewSpecCopy);
            } catch (DviException e1) {
              LOGGER.warning(e1.toString());
              try {
                recorder.append("Failed to load" + url);
              } catch (DviException e) {
                LOGGER.warning(e.toString());
              }
            } finally {
              item.close();
            }
            LOGGER.fine("End preparing fonts");
          } catch (DviException e1) {
            LOGGER.warning(e1.getMessage());
          }

          SwingUtilities.invokeLater(new Runnable() {
            public void run() {
              try {
                updateSize(doc);
                LOGGER.fine("Setting DVI document to " + doc);
                // tdd.setViewSpec(viewSpec);
                tdd.setDviDocument(doc);
                DviBrowserPage.this.url = url;
                progress.close();
              } catch (DviException e) {
                e.printStackTrace();
              } finally {
                loadIndicator.stop();
              }
            }
          });
        }
      });
    } catch (IOException e) {
      loadIndicator.stop();
      throw new DviException(e);
    }
  }
  
  public void reload()
  throws DviException
  {
    if (url == null) return;
    if ("file".equals(url.getProtocol())) {
      File file = new File(url.getPath());
      logViewer.setDviFile(file);
      if (null != tdd.getDviDocument() && logViewer.hasError()) {
        logViewer.setVisible(true);
        LOGGER.fine("Compilation error detected: " + url);
        return;
      } else {
        logViewer.setVisible(false);
      }
    } else {
      logViewer.setDviFile(null);
      LOGGER.warning("Unsupported protocol: " + url);
    }
    open(url);
  }
  
  protected JButton createButton(String name, Icon icon, String tooltip, String command)
  {
    return (JButton) decorateButton(new JButton(), name, icon, tooltip, command);
  }
  
  protected AbstractButton decorateButton(AbstractButton o, String name, Icon icon, String tooltip, String command)
  {
    if (icon == null) {
      o.setText(name);
    } else {
      o.setIcon(icon);
    }
    o.setToolTipText(tooltip);
    o.setActionCommand(command);
    o.addActionListener(this);
    return o;
  }

  protected JCheckBox createCheckBox(String name, Icon icon, String tooltip, String command)
  {
    return (JCheckBox) decorateButton(new JCheckBox(), name, icon, tooltip, command);
  }
  
  protected JComboBox createLayoutSelector()
  {
    JComboBox combo = new JComboBox();
    combo.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent e)
      {
        LOGGER.fine("layout setting changed");
        updateSize(null);
        try {
          tdd.reload();
        } catch (DviException e1) {
          LOGGER.warning(e1.toString());
        }
      }
    });
    List<DviBrowserPageLayout> layouts
      = new ArrayList<DviBrowserPageLayout>();
    populateLayoutSelector(layouts);
    combo.setModel(new DefaultComboBoxModel(layouts.toArray(new DviBrowserPageLayout[layouts.size()])));
    return combo; 
  }

  protected JComboBox createZoomSelector()
  {
    JComboBox combo = new JComboBox();
    combo.addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent e)
      {
        ZoomConfig conf = (ZoomConfig) (e.getItem());
        LOGGER.fine("zoom setting changed to " + conf);
        getViewSpec().setResolution(conf.getResolution());
        LOGGER.fine("new view spec " + getViewSpec());
        layoutSelector.setSelectedItem(freeLayout);
      }
    });
    List<ZoomConfig> list
      = new ArrayList<ZoomConfig>();
    populateZoomConfigs(list);
    combo.setModel(new DefaultComboBoxModel(list.toArray(new ZoomConfig[list.size()])));
    return combo; 
  }
  
  protected void populateZoomConfigs(List<ZoomConfig> list)
  {
    int [] sfs = {64, 33, 18, 12, 10, 8, 4};
    DviResolution res = getViewSpec().getResolution();
    for (int sf : sfs) {
      DviResolution res2 = new DviResolution(res.dpi(), sf);
      double screenDpi = 2400.0/33; // 72.72 dpi
      int ratio = (int)(res2.actualDpi() * 100 / screenDpi);
      String desc = String.format("%d%% (%ddpi)", ratio, (int)res2.actualDpi());
      list.add(new ZoomConfig(desc, res2));
    }
  }
  
  private DviBrowserPageLayout freeLayout;

  protected void populateLayoutSelector(List<DviBrowserPageLayout> layouts)
  {
    layouts.add(new DviBoxBrowserPageLayout(this, "Fit to width", true, false));
    layouts.add(new DviBoxBrowserPageLayout(this, "Fit to height", false, true));
    layouts.add(new DviBoxBrowserPageLayout(this, "Fit to both", true, true));
    layouts.add(freeLayout = new DviEmptyBrowserPageLayout(this, "Free"));
  }

  private void initializeCompontents()
  {
    tdd = new TDviDocument(this);
    layoutSelector = createLayoutSelector();
//    zoomSelector = createZoomSelector();
    checkBoxCrop = createCheckBox("Crop", null, "Crop margins", CMD_CROP_MARGINS);
    checkBoxCrop.setSelected(true);
    checkBoxTwoInOne = createCheckBox("2in1", null, "Display two pages horizontally", CMD_TWO_IN_ONE);
    closeButton = createButton
      ("Close", null, "Close this page", CMD_CLOSE_PAGE);
    
    JToolBar toolbar = new JToolBar("Navigator");
//    final JLabel indicatorLabel = new JLabel(loadIndicator);
//    loadIndicator.addChangeListener(new ChangeListener() {
//      public void stateChanged(ChangeEvent e)
//      {
//        SwingUtilities.invokeLater(new Runnable() {
//          public void run() {
//            indicatorLabel.repaint();
//          }
//        });
//      }
//    });
//    toolbar.add(indicatorLabel);
    toolbar.add(checkBoxCrop);
    toolbar.add(checkBoxTwoInOne);
    toolbar.add(layoutSelector);
//    toolbar.add(zoomSelector);
//    toolbar.add(createButton("PDF", null, "Open PDF file", CMD_OPEN_PDF));
    toolbar.add(new JPanel());
    toolbar.add(getCloseButton());
    toolbar.setRollover(true);
    toolbar.setFloatable(false);

    tdd = new TDviDocument(this);
    dragToScroll.add(tdd);
    getScrollPane().setViewportView(tdd);
    getScrollPane().getVerticalScrollBar().setUnitIncrement(32);
    getScrollPane().addComponentListener(new ComponentListener() {
      public void componentHidden(ComponentEvent e)
      {
      }
      public void componentMoved(ComponentEvent e)
      {
      }
      public void componentResized(ComponentEvent e)
      {
        updateSize(null);
        try {
          tdd.reload();
        } catch (DviException e1) {
          LOGGER.warning(e1.toString());
        }
      }
      public void componentShown(ComponentEvent e)
      {
      }
    });
    setLayout(new BorderLayout());
    logViewer.setVisible(false);
    add(logViewer, BorderLayout.SOUTH);
    add(getScrollPane(), BorderLayout.CENTER);
    add(toolbar, BorderLayout.NORTH);
    
    try {
      tdd.setViewSpec(viewSpec);
    } catch (DviException e2) {
      LOGGER.severe(e2.toString());
    }
    
    tdd.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        if (tdd.isBusy()) {
          loadIndicator.start();
        } else {
          loadIndicator.stop();
        }
      }
    });
    updateLayout();
  }
  
  public DviBrowserPageLayout getDviBrowserPageLayout()
  {
    Object o = layoutSelector.getSelectedItem();
    if (o instanceof DviBrowserPageLayout) {
      DviBrowserPageLayout layout = (DviBrowserPageLayout) o;
      return layout;
    } else {
      LOGGER.warning("Invalid browser page layout: " + o);
    }
    return null;
  }
  
  public void updateSize(DviDocument doc)
  {
    if (doc == null)
      doc = tdd.getDviDocument();
    if (doc == null) return;
    try {
      DviBrowserPageLayout layout = getDviBrowserPageLayout();
      if (layout != null) {
        layout.layoutDviBrowserPage(this, doc);
      } else {
        LOGGER.warning("No browser page layout available.");
      }
    } catch (DviException ex) {
      LOGGER.warning(ex.toString());
    }
  }
  
  public void handleEvent(final TEvent e)
  {
    if (e instanceof FileWatch.Modified) {
      LOGGER.fine("received event: " + e);
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          try {
            LOGGER.fine("reloading document: " + url);
            DviBrowserPage.this.getProgressRecorder().append("Reloaded: " + url);
            reload();
          } catch (DviException e) {
            LOGGER.warning(e.toString());
          }
        }
      });
      LOGGER.fine("Scheduled reload() to the EDT");
    }
  }
  
  public Dimension getViewportSize()
  {
    JScrollPane scroll = getScrollPane();

    int w = scroll.getViewport().getWidth();
    int h = scroll.getViewport().getHeight();
    if (!scroll.getVerticalScrollBar().isVisible()) {
      Dimension vs = scroll.getVerticalScrollBar().getPreferredSize();
      w -= vs.width;
    }
    if (!scroll.getHorizontalScrollBar().isVisible()) {
      Dimension hs = scroll.getHorizontalScrollBar().getPreferredSize();
      h -= hs.width;
    }
    
    return new Dimension(w, h);
  }
  
  public ViewSpec getViewSpec()
  {
    return viewSpec;
  }
  
  public TDviDocument getTDviDocument()
  {
    return tdd;
  }

  public ExecutorService getExecutorService()
  {
    return exe;
  }

  public ProgressRecorder getProgressRecorder()
  {
    return recorder;
  }

  public ProgressReporter getProgressReporter()
  {
    return reporter;
  }// JButton
  
  public void actionPerformed(ActionEvent e)
  {
    final String cmd = e.getActionCommand();
    if (CMD_OPEN_PDF.equals(cmd)) {
      // TODO: implement this
    } else if (CMD_CLOSE_PAGE.equals(cmd)) {
    } else if (CMD_SET_LAYOUT.equals(cmd)) {
    } else if (CMD_CROP_MARGINS.equals(cmd)) {
      updateLayout();
    } else if (CMD_TWO_IN_ONE.equals(cmd)) {
      updateLayout();
    } else {
      LOGGER.warning("Unrecognized action command: " + cmd);
    }
  }

  protected void updateLayout()
  {
    try {
      int cols = 1;
      if (checkBoxTwoInOne.isSelected()) {
        cols = 2;
      }
      boolean crop = false;
      if (checkBoxCrop.isSelected()) {
        crop = true;
      }
      // TODO: outsource the padding size.
      DefaultDviLayoutManager dlm = new DefaultDviLayoutManager(this, cols, 16);
      dlm.setEnableCrop(crop);
      tdd.setDviLayout(dlm);
      updateSize(null);
      tdd.reload();
    } catch (DviException e1) {
      LOGGER.warning(e1.toString());
    }
  }

  public JScrollPane getScrollPane()
  {
    return scroll;
  }

  public JButton getCloseButton()
  {
    return closeButton;
  }
  
  public DviLoadIndicator getLoadIndicator()
  {
    return loadIndicator;
  }

  public void setViewSpec(ViewSpec viewSpec)
  throws DviException
  {
    this.viewSpec = viewSpec;
    tdd.setViewSpec(viewSpec);
  }
}
